fix: hivectl choom enters as the agent user from the state dir so claude gets creds + session

This commit is contained in:
damocles 2026-06-05 21:41:50 +02:00
commit c59a0de01e
3 changed files with 62 additions and 17 deletions

View file

@ -73,11 +73,19 @@ enum Cmd {
},
/// Open an interactive Claude session inside an agent container.
///
/// Replaces the current process with `machinectl shell h-<name>`
/// running `claude --continue` — drops the operator straight into
/// the agent's live Claude session with its full loaded context and
/// persona. Requires root (same as all machinectl shell operations)
/// and the container must be running.
/// Replaces the current process with `machinectl shell
/// <name>@h-<name>` running `claude --continue` from the agent's
/// state dir — drops the operator straight into the agent's live
/// Claude session with its full loaded context and persona. Requires
/// root (same as all machinectl shell operations) and the container
/// must be running.
///
/// The session is entered **as the agent user** (not root): claude
/// reads its OAuth credentials + settings from the agent's
/// `/home/<name>/.claude`, and the working directory is set to the
/// agent's state dir (`/agents/<name>/state`) so `--continue`
/// resumes the same per-project session the harness runs. Entering
/// as root (the `machinectl shell` default) is what loses both.
///
/// Pass `--fresh` to start a new Claude session instead of continuing
/// the most recent one.
@ -357,10 +365,24 @@ fn is_agent(name: &str) -> bool {
/// Drop into an interactive Claude session in the agent container.
///
/// Replaces the current process (exec) with `machinectl shell h-<name>
/// /run/current-system/sw/bin/claude [--continue]`. The `--continue`
/// flag is passed by default so the operator joins the agent's active
/// session; omit it via `--fresh` to start a blank session instead.
/// Replaces the current process (exec) with `machinectl shell
/// <name>@h-<name> /bin/sh -lc 'cd /agents/<name>/state && exec claude
/// [--continue]'`. Two things matter here, both of which the naive
/// `machinectl shell h-<name> claude` got wrong (issue: choom missing
/// creds + settings):
///
/// 1. **Run as the agent user.** `machinectl shell` defaults to root in
/// the container, so claude would read `/root/.claude` (empty)
/// instead of the agent's `/home/<name>/.claude` where the OAuth
/// credentials + settings live. Prefixing the machine with
/// `<name>@` enters the session as the agent user (the meta-flake
/// sets `hyperhive.user.name` to the agent label, so the unix user
/// name matches the agent name).
/// 2. **Start in the agent's state dir.** `claude --continue` resumes
/// the most recent session *for the current project directory*. The
/// harness runs claude from `/agents/<name>/state`, so choom has to
/// `cd` there or `--continue` finds no session (it would look under
/// the user's home instead).
///
/// `machinectl shell` inherits the caller's PTY, so the session is
/// fully interactive. Requires root and a running container.
@ -369,12 +391,25 @@ fn choom(name: &str, fresh: bool) -> Result<()> {
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)");
}
let container = hive_c0re::lifecycle::container_name(name);
// The agent's unix user name matches its agent name (meta-flake
// sets `hyperhive.user.name = <label>`); enter the session as that
// user so claude sees the right `$HOME/.claude`.
let target = format!("{name}@{container}");
let claude = "/run/current-system/sw/bin/claude";
// In-container state dir (host `/var/lib/hyperhive/agents/<name>/state`
// is bind-mounted here); matches `hive-ag3nt::paths::state_dir()`.
let state_dir = format!("/agents/{name}/state");
let continue_flag = if fresh { "" } else { " --continue" };
// `cd` so `--continue` resolves the harness's per-project session;
// `exec` replaces the shell with claude so it owns the PTY directly.
// `name` is constrained to `[a-z0-9._-]` so there's nothing to quote.
let inner = format!("cd {state_dir} && exec {claude}{continue_flag}");
let mut cmd = std::process::Command::new("machinectl");
cmd.arg("shell").arg(&container).arg(claude);
if !fresh {
cmd.arg("--continue");
}
cmd.arg("shell")
.arg(&target)
.arg("/bin/sh")
.arg("-lc")
.arg(&inner);
// exec() replaces the current process — we inherit stdin/stdout/stderr
// (the caller's PTY) so the Claude session is fully interactive.
// This call only returns on error.