diff --git a/CLAUDE.md b/CLAUDE.md index 10991b55..96fa6871 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -184,8 +184,7 @@ hive-ag3nt/ in-container harness crate; produces TWO binaries `label()` / `qualified_label()` / `qualify(label)`. Reads `HYPERHIVE_HIVE_DOMAIN`; falls back to short name when unset. - src/login.rs probe $HOME/.claude/ (post-#658 `/home//.claude`) - for a valid session + src/login.rs probe /root/.claude/ for a valid session src/login_session.rs drives `claude auth login` over stdio pipes src/prompt.rs system prompt renderer: filters the unified template through `` / diff --git a/hive-ag3nt/src/login.rs b/hive-ag3nt/src/login.rs index b93b3caf..68578dbb 100644 --- a/hive-ag3nt/src/login.rs +++ b/hive-ag3nt/src/login.rs @@ -10,11 +10,9 @@ use std::path::{Path, PathBuf}; -/// Returns the Claude credentials directory for this agent. Delegates -/// to `paths::claude_dir`, which reads `$HOME/.claude` (post-#658 the -/// service runs as a non-root unix user named after the agent, so -/// `$HOME` resolves to `/home/` and the OAuth dir lives at -/// `/home//.claude`). Overridable via `HYPERHIVE_CLAUDE_DIR`. +/// Returns the Claude credentials directory for this agent, derived from +/// `HIVE_LABEL`. Manager ("hm1nd") uses `/root/.claude`; sub-agents use +/// `/agents/{label}/claude`. Overridable via `HYPERHIVE_CLAUDE_DIR`. #[must_use] pub fn default_dir() -> PathBuf { crate::paths::claude_dir() diff --git a/hive-ag3nt/src/login_session.rs b/hive-ag3nt/src/login_session.rs index 2860ebff..956375be 100644 --- a/hive-ag3nt/src/login_session.rs +++ b/hive-ag3nt/src/login_session.rs @@ -62,11 +62,9 @@ impl LoginSession { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - // `claude` reads $HOME/.claude for the credentials dir. The - // harness service env sets HOME to /home/ (post-#658) - // and the bind-mount lands the OAuth dir at the same path, - // so the child inherits the right HOME without any further - // wiring here. + // `claude` reads $HOME for the credentials dir; the bind-mount + // puts it at /root/.claude, which is already the default home + // for uid 0 inside the container. Nothing extra to set here. .kill_on_drop(true) .spawn() .with_context(|| format!("spawn `{cmd}`"))?; diff --git a/hive-ag3nt/src/paths.rs b/hive-ag3nt/src/paths.rs index edc7bad3..7564ebfb 100644 --- a/hive-ag3nt/src/paths.rs +++ b/hive-ag3nt/src/paths.rs @@ -1,9 +1,7 @@ //! Per-agent path resolution for state and credential directories. //! //! All agents (including the manager "hm1nd") use `/agents/{label}/state`. -//! Claude credentials live at `$HOME/.claude` (post-#658: -//! `/home//.claude` because the harness service now runs -//! as a non-root unix user matching the agent label). +//! Claude credentials are always at `/root/.claude` for all agents. //! //! Both paths can be overridden via env vars (`HYPERHIVE_STATE_DIR`, //! `HYPERHIVE_CLAUDE_DIR`) for dev / test scenarios. @@ -23,22 +21,12 @@ pub fn state_dir() -> PathBuf { PathBuf::from(format!("/agents/{label}/state")) } -/// Claude credentials directory for the current agent. `$HOME/.claude` -/// matches what the `claude` CLI reads at runtime — both binaries see -/// the same `$HOME` set by the per-service systemd `environment` -/// declaration (`/home/` post-#658). Falls back to `/root/.claude` -/// for dev / test environments where `HOME` isn't set so the previous -/// root-by-default shape keeps working without env wiring. +/// Claude credentials directory for the current agent. Always `/root/.claude` +/// because the `claude` CLI reads `$HOME/.claude` (uid 0 → `/root`), and +/// hive-c0re binds the per-agent credentials dir there for every container. /// Overridable via `HYPERHIVE_CLAUDE_DIR` for dev / test scenarios. #[must_use] pub fn claude_dir() -> PathBuf { - if let Some(p) = std::env::var_os("HYPERHIVE_CLAUDE_DIR") { - return PathBuf::from(p); - } - if let Some(home) = std::env::var_os("HOME") { - let mut path = PathBuf::from(home); - path.push(".claude"); - return path; - } - PathBuf::from("/root/.claude") + std::env::var_os("HYPERHIVE_CLAUDE_DIR") + .map_or_else(|| PathBuf::from("/root/.claude"), PathBuf::from) } diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 885b5a99..eddfc9e7 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -47,9 +47,8 @@ const RATE_LIMIT_MARKERS: &[&str] = &[ ]; /// Substrings that indicate the Anthropic API rejected the request as -/// unauthenticated — the OAuth session in `$HOME/.claude/` (post-#658 -/// `/home//.claude`, previously `/root/.claude`) has expired or -/// been revoked. Surfaced as `TurnOutcome::AuthFailed`, which the +/// unauthenticated — the OAuth session in `/root/.claude/` has expired +/// or been revoked. Surfaced as `TurnOutcome::AuthFailed`, which the /// harness uses to flip the container into `needs_login_idle` so the /// dashboard's re-auth flow takes over (closes #419). Matched against /// both stdout JSON `error` events and stderr; the markers come from @@ -164,13 +163,9 @@ pub async fn write_settings(socket: &Path) -> Result { // same socket-adjacent location every time and so a future override // (per-agent settings JSON layer) drops in cleanly. let src = hive_sh4re::assets::claude_settings(); - tokio::fs::copy(&src, &path).await.with_context(|| { - format!( - "copy claude settings from {} to {}", - src.display(), - path.display() - ) - })?; + tokio::fs::copy(&src, &path) + .await + .with_context(|| format!("copy claude settings from {} to {}", src.display(), path.display()))?; tracing::info!(path = %path.display(), "wrote claude settings"); Ok(path) } @@ -924,10 +919,7 @@ mod tests { // file_count=1 + no mtime, then writing a second file. let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("a"), b"{}").unwrap(); - let forged = DirSnapshot { - file_count: 1, - newest_mtime: None, - }; + let forged = DirSnapshot { file_count: 1, newest_mtime: None }; fs::write(dir.path().join("b"), b"{}").unwrap(); // Real snapshot has file_count=2, so refresh fires even // though the mtime axis would be inconclusive. diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index c81217a5..6e6f05c7 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -22,21 +22,9 @@ pub const MANAGER_PORT: u16 = 8000; /// Mount point of the per-agent runtime directory inside the container. pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive"; -/// Where the per-agent Claude credentials dir mounts inside the -/// container. Pre-#658 this was a constant (`/root/.claude`, because -/// every agent ran as root). With the user-named-after-agent shape -/// the harness service runs as a non-root unix user whose home is -/// `/home//`, so the mount path now varies per agent — -/// `container_claude_mount(name)` returns `/home//.claude` -/// for sub-agents and `/home/hm1nd/.claude` for the manager. -/// `claude` inside the container reads `$HOME/.claude` and the -/// service environment sets `HOME` to the same path, so the OAuth -/// session survives container restarts the same way the constant -/// did. -#[must_use] -pub fn container_claude_mount(name: &str) -> String { - format!("/home/{name}/.claude") -} +/// Mount point of the per-agent Claude credentials dir inside the container. +/// Persistent across destroy/recreate so OAuth login survives. +pub const CONTAINER_CLAUDE_MOUNT: &str = "/root/.claude"; /// Mount point of the shared directory accessible to all agents. /// All agents can read/write here; agents should only put things they're @@ -162,14 +150,7 @@ pub async fn spawn( // before `nixos-container create` so the `--flake meta#` // ref resolves. let agents = agents_after_spawn(name).await?; - crate::meta::sync_agents( - hyperhive_flake, - dashboard_port, - operator_pronouns, - context_window_tokens, - &agents, - ) - .await?; + crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, context_window_tokens, &agents).await?; let container = container_name(name); let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); run(&["create", &container, "--flake", &flake_ref]).await?; @@ -317,14 +298,7 @@ pub async fn rebuild( // got added directly via `nixos-container create` outside // hive-c0re). let agents = agents_for_meta(None).await?; - crate::meta::sync_agents( - hyperhive_flake, - dashboard_port, - operator_pronouns, - context_window_tokens, - &agents, - ) - .await?; + crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, context_window_tokens, &agents).await?; // Then bump just this agent's input — picks up whatever // `applied//main` currently points at (deployed/). // Commits the lock if it changed. @@ -886,14 +860,10 @@ fn set_nspawn_flags( // below are gated on `container == MANAGER_NAME` anyway. let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container); - // Claude credentials land at `/home//.claude` so the - // `claude` CLI (which reads `$HOME/.claude`) finds them. The - // harness service's environment sets `HOME` to the same path - // (`agent-base.nix` / `manager.nix`), so no `--setenv` plumbing - // is needed here — the bind alone is enough. Pre-#658 the mount - // was the constant `/root/.claude` because the service ran as - // root. - let claude_mount = container_claude_mount(agent_name); + // Claude credentials always land at /root/.claude so the + // `claude` CLI (which reads $HOME/.claude) finds them without + // any HOME override. + let claude_mount = CONTAINER_CLAUDE_MOUNT; let mut binds = format!( "--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{claude_mount} --bind={shared}:{CONTAINER_SHARED_MOUNT}", @@ -964,7 +934,8 @@ fn set_nspawn_flags( // `setup_proposed` seeds this dir before spawn reaches here, // but create defensively so a missing repo degrades to an // empty RO dir instead of a container that won't boot. - std::fs::create_dir_all(&config_dir).with_context(|| format!("create {config_dir}"))?; + std::fs::create_dir_all(&config_dir) + .with_context(|| format!("create {config_dir}"))?; let _ = write!(binds, " --bind-ro={config_dir}:/agents/{agent_name}/config"); } let bind_flag = format!("EXTRA_NSPAWN_FLAGS=\"{binds}\""); @@ -1167,9 +1138,6 @@ mod tests { .await .expect("git rev-list"); let count = String::from_utf8_lossy(&out.stdout).trim().to_owned(); - assert_eq!( - count, "1", - "expected exactly one commit after idempotent call" - ); + assert_eq!(count, "1", "expected exactly one commit after idempotent call"); } } diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 65b64fa0..9f313525 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -401,14 +401,6 @@ where modules = [ input.nixosModules.default { - # Drop root (#658): the harness service inside the - # container runs as a non-root unix user named after - # the agent (`damocles`, `iris`, `hm1nd`, …). UID - # auto-assigned by NixOS per mara on #658; the per- - # agent override here is what makes `hyperhive.user.name` - # match the agent's identity instead of the harness- - # base default of `"agent"`. - hyperhive.user.name = name; programs.git.config.user = { name = name; email = "${name}@hyperhive"; diff --git a/hive-matrix-mcp/src/paths.rs b/hive-matrix-mcp/src/paths.rs index 3b020646..7d497108 100644 --- a/hive-matrix-mcp/src/paths.rs +++ b/hive-matrix-mcp/src/paths.rs @@ -15,11 +15,10 @@ pub const DEFAULT_HOMESERVER: &str = "http://localhost:8008"; /// Default unix socket path the daemon listens on inside the agent /// container. The stdio MCP bridge `connect()`s here on every tool call. -/// Lives under systemd's `RuntimeDirectory=hive-matrix` (a tmpfs path -/// that disappears on container restart — fine, because the daemon -/// recreates the socket on its own boot) so the agent unix user -/// (post-#658) can bind a socket inside it without root in `/run`. -pub const DEFAULT_DAEMON_SOCKET: &str = "/run/hive-matrix/socket"; +/// `/run/hive-matrix.sock` is a tmpfs path that disappears on container +/// restart — fine, because the daemon recreates the socket on its own +/// boot. +pub const DEFAULT_DAEMON_SOCKET: &str = "/run/hive-matrix.sock"; /// Resolve the matrix access-token file path. Override via /// `HIVE_MATRIX_TOKEN_FILE`; default is `/matrix-token`, @@ -42,7 +41,7 @@ pub fn homeserver_url() -> String { } /// Resolve the daemon's unix socket path. Override via -/// `HIVE_MATRIX_SOCKET`; default is `/run/hive-matrix/socket`. +/// `HIVE_MATRIX_SOCKET`; default is `/run/hive-matrix.sock`. #[must_use] pub fn daemon_socket() -> PathBuf { std::env::var_os("HIVE_MATRIX_SOCKET").map_or_else(|| PathBuf::from(DEFAULT_DAEMON_SOCKET), PathBuf::from) diff --git a/nix/templates/agent-base.nix b/nix/templates/agent-base.nix index 53b1a315..4fc233bf 100644 --- a/nix/templates/agent-base.nix +++ b/nix/templates/agent-base.nix @@ -1,7 +1,4 @@ { pkgs, config, ... }: -let - userName = config.hyperhive.user.name; -in { imports = [ ./harness-base.nix ]; @@ -18,11 +15,6 @@ in path = [ "/run/current-system/sw" ]; environment = { SHELL = "${pkgs.bashInteractive}/bin/bash"; - # `HOME` defaults to `/` for systemd services without a User= - # set. With #658 the harness runs as the agent user — set HOME - # explicitly so claude (which the harness spawns) finds its - # `~/.claude/` session dir at the bind-mounted location. - HOME = "/home/${userName}"; # Path to the merged agent static dist. The harness serves this # via `tower_http::ServeDir` for any request it doesn't route to # an API endpoint. `mergedDist` is the agent-default dist with @@ -38,14 +30,6 @@ in ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt serve"; Restart = "on-failure"; RestartSec = 2; - # Run the harness as the per-agent user (#658). claude itself - # spawned by the harness then runs as that user too — drops - # root inside the container while sudo (`NOPASSWD: ALL` by - # default, see harness-base.nix `hyperhive.user.passwordlessSudo`) - # keeps the previous root-by-default surface available - # explicitly for tools that need it. - User = userName; - Group = userName; }; }; } diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 6b9331f1..f86e600c 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -9,15 +9,6 @@ flakeInputs ? { }, ... }: -let - # Agent user metadata (#658). `userName` defaults to `"agent"` when - # the meta-flake doesn't inject the per-agent override (stand-alone - # `nixos-rebuild` against `nixosConfigurations.agent-base` works - # without erroring on a missing per-agent name). `homeDir` derives - # from `userName` to keep them coupled. - userName = config.hyperhive.user.name; - homeDir = "/home/${userName}"; -in { # Shared scaffolding for any hyperhive harness container — both # sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend @@ -29,52 +20,6 @@ in # only opts in from its own `agent.nix`. imports = [ ./weston-vnc.nix ]; - # Per-agent unix user the harness + co-process daemons run as (#658). - # Defaults to `"agent"` so a standalone evaluation (e.g. - # `nix flake check` against `nixosConfigurations.agent-base`) builds - # cleanly; the meta-flake's per-agent module rebinds this to the - # agent name (`"damocles"`, `"iris"`, …) so each container has a - # uniquely-named user matching its agent label. UID auto-assigned - # by NixOS (the auto-allocation range for normal users); no hard- - # coded UID. - options.hyperhive.user.name = lib.mkOption { - type = lib.types.strMatching "^[a-z_][a-z0-9_-]{0,30}$"; - default = "agent"; - example = "iris"; - description = '' - Unix user the harness service runs as inside the container. - The meta-flake overrides this to the agent's own name so the - user inside the container matches the agent label (`HIVE_LABEL`). - Stand-alone evaluation defaults to `"agent"` so module evaluation - without the meta-flake wrapper still builds. - - Constraints match `useradd`'s NAME_REGEX: lowercase / `_` start, - total length ≤ 31, no special characters. UID is auto-assigned - by NixOS; no `uid =` override surface (intentional — pinning - across rebuilds isn't a concern when the home and state dirs - stay bind-mounted from the host). - ''; - }; - - options.hyperhive.user.passwordlessSudo = lib.mkOption { - type = lib.types.bool; - default = true; - example = false; - description = '' - Grant `${config.hyperhive.user.name}` passwordless sudo - (`NOPASSWD: ALL`). True by default so claude's `Bash` tool - keeps working for tools that expect root inside the container - (`systemctl`, package managers in dev shells, etc.) — the - same surface the previous root-user shape had, just elevated - explicitly instead of implicitly. - - Flip to `false` for agents that should be strictly - unprivileged. Anything claude shells out to that needs root - will then fail loudly with the standard sudo error rather - than silently succeeding — easier to spot the leak. - ''; - }; - options.hyperhive.model = lib.mkOption { type = lib.types.str; default = "haiku"; @@ -408,25 +353,23 @@ in }; options.hyperhive.dashboardLinks = lib.mkOption { - type = lib.types.listOf ( - lib.types.submodule { - options = { - label = lib.mkOption { - type = lib.types.str; - description = "Display label for the link."; - }; - icon = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Optional icon emoji or short glyph."; - }; - url = lib.mkOption { - type = lib.types.str; - description = "Full URL (may include a different port, e.g. http://localhost:9001/stats)."; - }; + type = lib.types.listOf (lib.types.submodule { + options = { + label = lib.mkOption { + type = lib.types.str; + description = "Display label for the link."; }; - } - ); + icon = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Optional icon emoji or short glyph."; + }; + url = lib.mkOption { + type = lib.types.str; + description = "Full URL (may include a different port, e.g. http://localhost:9001/stats)."; + }; + }; + }); default = [ ]; example = lib.literalExpression '' [ @@ -562,7 +505,9 @@ in } # hyperhive.icon must reference an SVG file when set. { - assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon); + assertion = + config.hyperhive.icon == null + || lib.hasSuffix ".svg" (toString config.hyperhive.icon); message = "hyperhive.icon must point to an .svg file"; } # hyperhive.frontend.extraFiles[*].target is concatenated into @@ -574,9 +519,9 @@ in # operator-reviewed, so this is belt-and-braces — but it's the # kind of mistake that's easy to make and hard to spot. { - assertion = lib.all (entry: !(builtins.any (seg: seg == "..") (lib.splitString "/" entry.target))) ( - lib.attrValues config.hyperhive.frontend.extraFiles - ); + assertion = lib.all ( + entry: !(builtins.any (seg: seg == "..") (lib.splitString "/" entry.target)) + ) (lib.attrValues config.hyperhive.frontend.extraFiles); message = '' hyperhive.frontend.extraFiles: `target` must not contain `..` path segments. @@ -584,111 +529,6 @@ in } ]; - # Per-agent unix user (#658). Runs the hive-ag3nt / hive-m1nd - # harness + co-process daemons (hive-matrix-daemon) under a - # non-root principal. The user name follows - # `hyperhive.user.name` — defaults to `"agent"` for standalone - # eval, overridden per-agent by the meta-flake to the agent's - # own label so each container has a uniquely-named user. - # - # UID auto-assigned by NixOS (per mara's #8109: "no hardcoded - # uids"). Home is `/home/${userName}`. `wheel` membership + - # the sudoers rule below grants `NOPASSWD: ALL` when - # `passwordlessSudo` is true — same blast radius as the - # previous root-by-default shape, just explicit. - users.users.${userName} = { - isNormalUser = true; - home = homeDir; - createHome = true; - group = userName; - extraGroups = lib.optional config.hyperhive.user.passwordlessSudo "wheel"; - # Matches /bin/bash on NixOS — the harness's claude shell-outs - # expect a POSIX shell at $SHELL; bashInteractive is already - # the system default for the root user too (see SHELL env - # var declaration below). - shell = pkgs.bashInteractive; - }; - users.groups.${userName} = { }; - - # `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool - # keep working with anything that expected root (systemctl, - # nix-env, etc.) without prompting — same surface as the - # previous root-by-default shape, just elevated explicitly. - # Flip `hyperhive.user.passwordlessSudo = false` to drop both - # the wheel-group membership and this sudoers entry; anything - # that needs root then fails loudly instead of silently - # succeeding. - security.sudo.extraRules = lib.mkIf config.hyperhive.user.passwordlessSudo [ - { - users = [ userName ]; - commands = [ - { - command = "ALL"; - options = [ "NOPASSWD" ]; - } - ]; - } - ]; - - # 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. - 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" - fi - 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 - ''; - # Auto-inject the matrix MCP entry when matrix is enabled (#548 # phase 3). Operator can override or disable by setting their own # `extraMcpServers.matrix` (nix submodule merge takes the operator's @@ -697,11 +537,7 @@ in matrix = lib.mkDefault { command = "${pkgs.hyperhive}/bin/hive-matrix-mcp"; args = [ ]; - # Same socket path the hive-matrix-daemon service binds - # via its `RuntimeDirectory = "hive-matrix"` (#658). Keeps - # the bridge + daemon in sync without baking the new path - # into the Rust default — the env override wins for both. - env.HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket"; + env = { }; allowedTools = [ "*" ]; }; }; @@ -711,9 +547,8 @@ in # Operator-set per-agent icon (hyperhive.icon). When configured, the # SVG lands at /etc/hyperhive/icon.svg; the harness serves it at # GET /icon, falling back to the bundled hyperhive logo when absent. - environment.etc."hyperhive/icon.svg" = lib.mkIf (config.hyperhive.icon != null) { - source = config.hyperhive.icon; - }; + environment.etc."hyperhive/icon.svg" = + lib.mkIf (config.hyperhive.icon != null) { source = config.hyperhive.icon; }; environment.etc."hyperhive/bash-allow.json".text = builtins.toJSON config.hyperhive.allowedBashPatterns; @@ -765,16 +600,13 @@ in HIVE_DEFAULT_MODEL = config.hyperhive.model; HIVE_ASSETS_DIR = "${pkgs.hyperhive-assets}/share/hyperhive"; SHELL = "${pkgs.bashInteractive}/bin/bash"; - } - // lib.optionalAttrs (!config.hyperhive.autoCompact) { + } // lib.optionalAttrs (!config.hyperhive.autoCompact) { # Zero watermark disables proactive compaction; the reactive path # (compact-on-overflow) still fires when the session is truly full. HIVE_COMPACT_WATERMARK_TOKENS = "0"; - } - // lib.optionalAttrs config.hyperhive.forge.keepSubscriptions { + } // lib.optionalAttrs config.hyperhive.forge.keepSubscriptions { HIVE_FORGE_KEEP_SUBSCRIPTIONS = "1"; - } - // lib.optionalAttrs (config.hyperhive.forge.skipNotifyReasons != [ ]) { + } // lib.optionalAttrs (config.hyperhive.forge.skipNotifyReasons != [ ]) { HIVE_FORGE_NOTIFY_SKIP_REASONS = lib.concatStringsSep "," config.hyperhive.forge.skipNotifyReasons; }; @@ -848,8 +680,6 @@ in pkgs.python3 pkgs.coreutils ]; - environment.HOME_DIR = homeDir; - environment.AGENT_USER = userName; script = '' # No `set -e`: any subshell failure must not propagate. # A failed unit aborts `nixos-container update` which blocks rebuilds. @@ -873,16 +703,13 @@ in echo "tea-login: could not resolve username from forge API; skipping" exit 0 fi - # tea reads config from ~/.config/tea/config.yml. The - # agent user's home is $HOME_DIR (set by NixOS via - # hyperhive.user.name). Write the config under that - # home + chown to the agent user so tea reads it when - # invoked as that user. Still runs as root (this - # service stays root-owned to avoid bootstrap - # ordering issues — see comment near serviceConfig - # below), but the artefact it produces is for the - # agent user. - CONFIG="$HOME_DIR/.config/tea/config.yml" + # tea reads config from ~/.config/tea/config.yml (for root: /root/.config/tea/config.yml). + # Write it directly so we control default:true and always + # refresh a rotated token — no 'tea login add' interactive dance. + # $HOME is unset in systemd service context (causing writes to + # /.config/). Hardcode /root — always correct for NixOS containers + # where the harness runs as root. + CONFIG="/root/.config/tea/config.yml" mkdir -p "$(dirname "$CONFIG")" || true cat > "$CONFIG" << EOF logins: @@ -900,8 +727,7 @@ in flag_defaults: remote: "" EOF - chown -R "$AGENT_USER:$AGENT_USER" "$HOME_DIR/.config" 2>/dev/null || true - echo "tea-login: configured for $FORGE_URL as $USER (config at $CONFIG)" + echo "tea-login: configured for $FORGE_URL as $USER" ''; }; @@ -982,27 +808,16 @@ 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"; }; serviceConfig = { 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"; + # /run/hive-matrix.sock + the matrix-sdk-state sqlite dir + # don't need a StateDirectory= — the socket is on tmpfs (gone + # on restart, which is correct) and the sqlite dir lives in + # the bind-mounted agent state, mode-managed by the harness. }; }; diff --git a/nix/templates/manager.nix b/nix/templates/manager.nix index 2e09dbdd..55d94805 100644 --- a/nix/templates/manager.nix +++ b/nix/templates/manager.nix @@ -1,7 +1,4 @@ { pkgs, config, ... }: -let - userName = config.hyperhive.user.name; -in { imports = [ ./harness-base.nix ]; @@ -27,9 +24,6 @@ in HIVE_PORT = "8000"; HIVE_LABEL = "hm1nd"; SHELL = "${pkgs.bashInteractive}/bin/bash"; - # `HOME` set explicitly so claude finds `~/.claude/` at the - # bind-mounted location after #658 (User= drop from root). - HOME = "/home/${userName}"; # Manager runs the same hive-m1nd harness binary that serves # the per-agent web UI; point it at the merged agent static dist # (same shape as for sub-agents). @@ -48,13 +42,6 @@ in ExecStart = "${pkgs.hyperhive}/bin/hive-m1nd serve"; Restart = "on-failure"; RestartSec = 2; - # Same drop-from-root as agent-base.nix (#658). Manager - # interactions with the host (rebuild approvals, config - # writes) still happen via the dedicated unix sockets - # bind-mounted from hive-c0re — those don't need root - # inside the container. - User = userName; - Group = userName; }; }; }