//! `hivectl choom ` — drop into an interactive Claude session inside //! an agent container by exec-ing `machinectl shell` running claude as the //! agent user, mirroring the harness's per-turn claude invocation. #[cfg(unix)] use std::os::unix::process::CommandExt as _; use anyhow::{Result, bail}; use crate::util::agent_exists; /// Drop into an interactive Claude session in the agent container. /// /// Execs `machinectl shell @h-` running claude as the agent /// user from its state dir, reproducing the harness's per-turn claude /// invocation (flags, session selection, why it never collides with the /// live harness session). See `docs/tools/hivectl.md` (Choom) for the /// full rationale. Inherits the caller's PTY; requires root + a running /// container. pub(crate) fn choom(name: &str, resume_session: Option<&str>) -> Result<()> { if !agent_exists(name)? { bail!( "no such agent: '{name}' (no state dir under {}/)", hive_host_sock::AGENTS_ROOT ); } let container = hive_host_sock::container_name(name); // Enter as the agent's unix user (== agent name) so claude reads the // right `$HOME/.claude`. let target = format!("{name}@{container}"); let claude = "/run/current-system/sw/bin/claude"; // Bind-mounted state dir; matches `hive-agent::paths::state_dir()`. let state_dir = format!("/agents/{name}/state"); // Per-turn config the harness writes; matches `paths::config_dir()`. let cfg = "/run/hive-config"; // `--resume ` passes through as `claude --resume ` — // the rejoin-by-id surface. (claude's `--continue` is a bare flag // that resumes the cwd's latest session — the harness's — and would // consume a trailing value as the first PROMPT; choom never uses // it.) The value is single-quoted into the shell script, so reject // an embedded single quote (the only char that breaks // single-quoting) to rule out injection — session ids never // contain one. let session_arg = match resume_session { Some(val) => { if val.contains('\'') { bail!("invalid --resume value '{val}': must not contain a single quote"); } format!("set -- --resume '{val}';") } None => "set --;".to_string(), }; // Build claude's argv as the harness does, each flag included only // when its file is present so a half-up container degrades to a bare // session. `name` is `[a-z0-9._-]` so there's nothing to quote. let inner = format!( "cd {state_dir} || exit 1; {session_arg} \ [ -f {cfg}/claude-settings.json ] && set -- \"$@\" --settings {cfg}/claude-settings.json; \ [ -f {cfg}/claude-mcp-config.json ] && set -- \"$@\" --mcp-config {cfg}/claude-mcp-config.json; \ [ -f {cfg}/claude-system-prompt.md ] && set -- \"$@\" --system-prompt-file {cfg}/claude-system-prompt.md; \ exec {claude} \"$@\"" ); let mut cmd = std::process::Command::new("machinectl"); 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. let err = cmd.exec(); Err(anyhow::anyhow!("exec machinectl: {err}")) }