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
|
|
@ -22,9 +22,20 @@ 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";
|
||||
|
||||
/// 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";
|
||||
/// 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/<agent>/`, so the mount path now varies per agent —
|
||||
/// `container_claude_mount(name)` returns `/home/<name>/.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.
|
||||
pub fn container_claude_mount(name: &str) -> String {
|
||||
format!("/home/{name}/.claude")
|
||||
}
|
||||
|
||||
/// Mount point of the shared directory accessible to all agents.
|
||||
/// All agents can read/write here; agents should only put things they're
|
||||
|
|
@ -150,7 +161,14 @@ pub async fn spawn(
|
|||
// before `nixos-container create` so the `--flake meta#<name>`
|
||||
// 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?;
|
||||
|
|
@ -298,7 +316,14 @@ 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/<n>/main` currently points at (deployed/<latest>).
|
||||
// Commits the lock if it changed.
|
||||
|
|
@ -860,10 +885,14 @@ 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 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;
|
||||
// Claude credentials land at `/home/<agent>/.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);
|
||||
|
||||
let mut binds = format!(
|
||||
"--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{claude_mount} --bind={shared}:{CONTAINER_SHARED_MOUNT}",
|
||||
|
|
@ -934,8 +963,7 @@ 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}\"");
|
||||
|
|
@ -1138,6 +1166,9 @@ 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -401,6 +401,14 @@ 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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue