split hivectl main.rs into per-domain modules (#2509)
This commit is contained in:
parent
673aea4e50
commit
fc7720572b
16 changed files with 1922 additions and 1771 deletions
74
hivectl/src/choom.rs
Normal file
74
hivectl/src/choom.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
//! `hivectl choom <agent>` — 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 <name>@h-<name>` 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 <value>` passes through as `claude --resume <value>` —
|
||||
// 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}"))
|
||||
}
|
||||
Loading…
Reference in a new issue