agents: drop root, run as per-agent unix user with passwordless sudo (#658)
This commit is contained in:
parent
71211e5722
commit
6b6c6775ee
10 changed files with 349 additions and 71 deletions
|
|
@ -10,9 +10,11 @@
|
|||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 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`.
|
||||
/// 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/<agent>` and the OAuth dir lives at
|
||||
/// `/home/<agent>/.claude`). Overridable via `HYPERHIVE_CLAUDE_DIR`.
|
||||
#[must_use]
|
||||
pub fn default_dir() -> PathBuf {
|
||||
crate::paths::claude_dir()
|
||||
|
|
|
|||
|
|
@ -62,9 +62,11 @@ impl LoginSession {
|
|||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
// `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.
|
||||
// `claude` reads $HOME/.claude for the credentials dir. The
|
||||
// harness service env sets HOME to /home/<agent> (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.
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.with_context(|| format!("spawn `{cmd}`"))?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
//! Per-agent path resolution for state and credential directories.
|
||||
//!
|
||||
//! All agents (including the manager "hm1nd") use `/agents/{label}/state`.
|
||||
//! Claude credentials are always at `/root/.claude` for all agents.
|
||||
//! Claude credentials live at `$HOME/.claude` (post-#658:
|
||||
//! `/home/<agent-name>/.claude` because the harness service now runs
|
||||
//! as a non-root unix user matching the agent label).
|
||||
//!
|
||||
//! Both paths can be overridden via env vars (`HYPERHIVE_STATE_DIR`,
|
||||
//! `HYPERHIVE_CLAUDE_DIR`) for dev / test scenarios.
|
||||
|
|
@ -21,12 +23,22 @@ pub fn state_dir() -> PathBuf {
|
|||
PathBuf::from(format!("/agents/{label}/state"))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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/<agent>` 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.
|
||||
/// Overridable via `HYPERHIVE_CLAUDE_DIR` for dev / test scenarios.
|
||||
#[must_use]
|
||||
pub fn claude_dir() -> PathBuf {
|
||||
std::env::var_os("HYPERHIVE_CLAUDE_DIR")
|
||||
.map_or_else(|| PathBuf::from("/root/.claude"), PathBuf::from)
|
||||
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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,9 @@ const RATE_LIMIT_MARKERS: &[&str] = &[
|
|||
];
|
||||
|
||||
/// Substrings that indicate the Anthropic API rejected the request as
|
||||
/// unauthenticated — the OAuth session in `/root/.claude/` has expired
|
||||
/// or been revoked. Surfaced as `TurnOutcome::AuthFailed`, which the
|
||||
/// unauthenticated — the OAuth session in `$HOME/.claude/` (post-#658
|
||||
/// `/home/<agent>/.claude`, previously `/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
|
||||
|
|
@ -163,9 +164,13 @@ pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -919,7 +924,10 @@ 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue