diff --git a/hive-ag3nt/src/paths.rs b/hive-ag3nt/src/paths.rs index edc7bad3..21e4aa1c 100644 --- a/hive-ag3nt/src/paths.rs +++ b/hive-ag3nt/src/paths.rs @@ -23,6 +23,22 @@ pub fn state_dir() -> PathBuf { PathBuf::from(format!("/agents/{label}/state")) } +/// Per-turn config dir for the regenerated claude-{mcp-config,settings, +/// system-prompt} files the harness drops before each turn. Set by +/// systemd via `RuntimeDirectory = "hive-config"` (#658 fixup): a +/// per-service runtime dir owned by the agent unix user, auto-cleared +/// on stop. Kept separate from `/run/hive` (the host-owned mcp.sock +/// bind) so the harness owns its own write surface and we don't have +/// to chown a bind-mounted dir. Overridable via `HYPERHIVE_CONFIG_DIR` +/// for dev / test scenarios. +#[must_use] +pub fn config_dir() -> PathBuf { + if let Some(p) = std::env::var_os("HYPERHIVE_CONFIG_DIR") { + return PathBuf::from(p); + } + PathBuf::from("/run/hive-config") +} + /// 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` diff --git a/hive-ag3nt/src/prompt.rs b/hive-ag3nt/src/prompt.rs index 6178e015..9b6ff5e1 100644 --- a/hive-ag3nt/src/prompt.rs +++ b/hive-ag3nt/src/prompt.rs @@ -126,9 +126,9 @@ fn parse_close_marker(line: &str) -> Option<&str> { /// # Errors /// /// Returns an error if the system prompt file cannot be written. -pub async fn write_system_prompt(socket: &Path, label: &str, flavor: Flavor) -> Result { - let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive")); - tokio::fs::create_dir_all(parent).await.ok(); +pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) -> Result { + let parent = crate::paths::config_dir(); + tokio::fs::create_dir_all(&parent).await.ok(); let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned()); let template_path = hive_sh4re::assets::prompt_template(); let template = tokio::fs::read_to_string(&template_path) diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 885b5a99..95c9f0d8 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -136,8 +136,8 @@ impl TurnFiles { /// /// Returns an error if the config file cannot be written. pub async fn write_mcp_config(socket: &Path) -> Result { - let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive")); - tokio::fs::create_dir_all(parent).await.ok(); + let parent = crate::paths::config_dir(); + tokio::fs::create_dir_all(&parent).await.ok(); let path = parent.join("claude-mcp-config.json"); let exe = std::env::current_exe() .ok() @@ -155,9 +155,9 @@ pub async fn write_mcp_config(socket: &Path) -> Result { /// # Errors /// /// Returns an error if the settings file cannot be written. -pub async fn write_settings(socket: &Path) -> Result { - let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive")); - tokio::fs::create_dir_all(parent).await.ok(); +pub async fn write_settings(_socket: &Path) -> Result { + let parent = crate::paths::config_dir(); + tokio::fs::create_dir_all(&parent).await.ok(); let path = parent.join("claude-settings.json"); // #555: source-of-truth is `$HIVE_ASSETS_DIR/prompts/claude-settings.json`; // copy through the per-agent runtime dir so claude reads it from the diff --git a/nix/templates/agent-base.nix b/nix/templates/agent-base.nix index ffbc826b..01fceb1a 100644 --- a/nix/templates/agent-base.nix +++ b/nix/templates/agent-base.nix @@ -38,14 +38,14 @@ in ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt serve"; Restart = "on-failure"; RestartSec = 2; - # `/run/hive` is bind-mounted from the host root-owned 0755 - # (hive-c0re's `set_nspawn_flags`). Post-#658 the harness - # runs as the per-agent user and needs to drop mcp.sock + - # claude-{mcp-config,settings,system-prompt} files there. - # `+` runs ExecStartPre as root (before the User= drop) so - # we can chown the bind onto the agent user every start — - # robust against activation-script timing on first boot. - ExecStartPre = "+${pkgs.coreutils}/bin/chown -R ${userName}:${userName} /run/hive"; + # `/run/hive-config/` is a per-service runtime dir owned by + # the agent user (`User=` below), auto-cleared by systemd on + # stop. The harness writes its regenerated + # claude-{mcp-config,settings,system-prompt} files there + # (see `paths::config_dir`). Kept separate from `/run/hive` + # — that bind comes in root-owned from the host and holds + # hive-c0re's `mcp.sock` we only connect to (#658 fixup). + RuntimeDirectory = "hive-config"; # 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 diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 6b9331f1..cc4b0bbb 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -687,6 +687,18 @@ in [ -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 ''; # Auto-inject the matrix MCP entry when matrix is enabled (#548 diff --git a/nix/templates/manager.nix b/nix/templates/manager.nix index 394ab374..66e9035d 100644 --- a/nix/templates/manager.nix +++ b/nix/templates/manager.nix @@ -48,14 +48,14 @@ in ExecStart = "${pkgs.hyperhive}/bin/hive-m1nd serve"; Restart = "on-failure"; RestartSec = 2; - # `/run/hive` is bind-mounted from the host root-owned 0755 - # (hive-c0re's `set_nspawn_flags`). Post-#658 the harness - # runs as the per-agent user and needs to drop mcp.sock + - # claude-{mcp-config,settings,system-prompt} files there. - # `+` runs ExecStartPre as root (before the User= drop) so - # we can chown the bind onto the agent user every start — - # robust against activation-script timing on first boot. - ExecStartPre = "+${pkgs.coreutils}/bin/chown -R ${userName}:${userName} /run/hive"; + # `/run/hive-config/` is a per-service runtime dir owned by + # the agent user (`User=` below), auto-cleared by systemd on + # stop. The harness writes its regenerated + # claude-{mcp-config,settings,system-prompt} files there + # (see `paths::config_dir`). Kept separate from `/run/hive` + # — that bind comes in root-owned from the host and holds + # hive-c0re's `mcp.sock` we only connect to (#658 fixup). + RuntimeDirectory = "hive-config"; # 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