Huginn/crates/mind-body/src/cli.rs
Meta Vens edfbf9ce25 mind-control: swarm brake hold stops every Mind's units, through the one stopper
stop_all becomes SystemdLauncher::stop(StopScope): a Mind's two exact units,
or the swarm's mind-self@*.service and mind-persona@*.service globs. Both
holds write first, then stop whatever the write's outcome, with the same four
outcomes. Soul s1 swarm-hold-leaves-live-turn-running: a held swarm brake left
a live Persona turn running with its shell until the turn ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:11:21 +01:00

213 lines
9.3 KiB
Rust

//! The `mind-control` command line: the operator's hand on one instance's
//! control store. It checks no caller identity: the file is
//! root-owned and the filesystem is the authority.
//!
//! ```text
//! mind-control [--root <dir>] --instance <slug> show
//! mind-control --instance <slug> brake hold (also stops her running units)
//! mind-control --instance <slug> brake release
//! mind-control [--root <dir>] --instance <slug> check
//! mind-control [--root <dir>] swarm show
//! mind-control swarm brake hold | release (hold also stops every Mind's units)
//! ```
//!
//! The `swarm` verbs write `<root>/swarm.cc`; `swarm brake hold` also stops
//! every Mind's units, and `swarm brake release` stops nothing.
//!
//! `check` exits 0 only when the instance brake and the swarm brake are released; a
//! systemd unit runs it as its `ExecCondition`. It writes nothing.
//!
//! Either `brake hold` writes its brake held and then, whatever the write's
//! outcome, stops the units it covers through the one `stop_units` parameter
//! (a Mind's two units, or every Mind's). It fails if either step failed. No
//! other command stops anything.
use std::io::Write;
use std::net::SocketAddr;
use std::path::Path;
use anyhow::{Result, anyhow, bail};
use chrono::{DateTime, Utc};
use eureka_pipeline::{RunTurn, Slug};
use crate::control::{
ControlWriter, Effective, FileSource, SwarmWriter, control_path, derive, derive_swarm, load_state, load_swarm_state, swarm_path,
};
use crate::launch::{StopScope, unit};
const USAGE: &str =
"usage: mind-control [--root <dir>] --instance <slug> show | check | brake hold | brake release | mind-control [--root <dir>] swarm show | brake hold | brake release";
/// The arguments every organ binary shares: `--instance <slug>` and an optional
/// `--endpoint rudp://<ip>:<port>`, which `env_endpoint` (the binary's reading of
/// `HUGINN_ENDPOINT`) replaces when absent. A flag given twice, an unknown
/// argument, a name that is not a slug or an endpoint that is not
/// `rudp://<ip>:<port>` is refused with `usage` or a fixed message, and no
/// value is echoed.
pub fn instance_and_endpoint(args: &[String], env_endpoint: Option<String>, usage: &str) -> Result<(Slug, SocketAddr)> {
let (mut instance, mut endpoint) = (None, None);
let mut rest = args.iter();
while let Some(flag) = rest.next() {
let slot = match flag.as_str() {
"--instance" => &mut instance,
"--endpoint" => &mut endpoint,
_ => bail!("{usage}"),
};
let value = rest.next().ok_or_else(|| anyhow!("{usage}"))?;
if slot.replace(value.clone()).is_some() {
bail!("{usage}");
}
}
let instance = Slug(instance.ok_or_else(|| anyhow!("{usage}"))?);
instance.validate_slug().map_err(|_| anyhow!("the instance is not a slug"))?;
let endpoint = endpoint.or(env_endpoint).ok_or_else(|| anyhow!("no daemon endpoint: --endpoint or HUGINN_ENDPOINT"))?;
let address = endpoint
.strip_prefix("rudp://")
.and_then(|address| address.parse::<SocketAddr>().ok())
.ok_or_else(|| anyhow!("the endpoint is not rudp://<ip>:<port>"))?;
Ok((instance, address))
}
/// Who made the change: `SUDO_USER`, else `USER`, else `unknown`. An empty
/// variable counts as unset.
pub fn operator_name(sudo_user: Option<String>, user: Option<String>) -> String {
[sudo_user, user].into_iter().flatten().find(|name| !name.is_empty()).unwrap_or_else(|| "unknown".to_string())
}
/// Runs one command against `<root>/<instance>/control.cc`. `root` is the
/// default; a leading `--root <dir>` argument replaces it (argv, not environment,
/// because `sudo -E` carries the environment). The operator's name, the clock, the output and the stopper are parameters so a test drives
/// the whole command without touching `/etc` or systemd. `stop_units` is
/// called only by a `brake hold`, after the write, whatever the write's outcome.
pub fn run(
args: &[String],
root: &Path,
set_by: &str,
now: DateTime<Utc>,
out: &mut impl Write,
stop_units: &mut dyn FnMut(StopScope<'_>) -> Result<()>,
) -> Result<()> {
let (root, args) = match args {
[flag, dir, rest @ ..] if flag == "--root" => (Path::new(dir), rest),
_ => (root, args),
};
if let [word, rest @ ..] = args
&& word == "swarm"
{
return swarm(root, rest, set_by, now, out, stop_units);
}
let (instance, command) = match args {
[flag, instance, command @ ..] if flag == "--instance" => (Slug(instance.clone()), command),
_ => bail!("{USAGE}"),
};
let path = control_path(root, &instance)?;
let words: Vec<&str> = command.iter().map(String::as_str).collect();
match words.as_slice() {
["show"] => show(root, &instance, out),
["check"] => check(root, &path, &instance, out),
["brake", "hold"] => {
let written = ControlWriter::open(&path).and_then(|mut writer| writer.set_brake(false, now, set_by));
let stopped = stop_units(StopScope::Mind(&instance));
match (written, stopped) {
(Ok(()), Ok(())) => {
writeln!(out, "units: stopped {} {}", unit(&instance, RunTurn::SelfRun)?, unit(&instance, RunTurn::PersonaTurn)?)?;
Ok(())
}
outcome => held(outcome),
}
}
["brake", "release"] => ControlWriter::open(&path)?.set_brake(true, now, set_by),
_ => bail!("{USAGE}"),
}
}
/// A hold whose write or stop failed: what was written, what was stopped, and
/// which step failed, the same words for a Mind's hold and the swarm's.
fn held(outcome: (Result<()>, Result<()>)) -> Result<()> {
match outcome {
(Ok(()), Ok(())) => Ok(()),
(Err(write), Ok(())) => Err(write.context("the brake was not written; the units were stopped")),
(Ok(()), Err(stop)) => Err(stop.context("the brake is held; the stop failed")),
(Err(write), Err(stop)) => Err(anyhow!("the brake was not written ({write:#}) and the stop failed ({stop:#})")),
}
}
/// The `swarm` verbs over `<root>/swarm.cc`. Every refusal happens before the
/// writer is opened, so a refused command creates nothing. `brake hold` then
/// stops every Mind's units, whatever the write's outcome.
fn swarm(
root: &Path,
args: &[String],
set_by: &str,
now: DateTime<Utc>,
out: &mut impl Write,
stop_units: &mut dyn FnMut(StopScope<'_>) -> Result<()>,
) -> Result<()> {
let path = swarm_path(root);
let words: Vec<&str> = args.iter().map(String::as_str).collect();
match words.as_slice() {
["show"] => show_swarm(&path, out),
["brake", "hold"] => {
let written = SwarmWriter::open(&path).and_then(|mut writer| writer.set_brake(false, now, set_by));
let stopped = stop_units(StopScope::Swarm);
if written.is_ok() && stopped.is_ok() {
writeln!(out, "units: stopped every mind-self@ and mind-persona@ unit")?;
}
held((written, stopped))
}
["brake", "release"] => SwarmWriter::open(&path)?.set_brake(true, now, set_by),
_ => bail!("{USAGE}"),
}
}
fn show_swarm(path: &Path, out: &mut impl Write) -> Result<()> {
let source = FileSource::new(path);
writeln!(out, "swarm: {}", path.display())?;
match load_swarm_state(&source) {
Ok(state) => {
match &state.brake {
Some(brake) => writeln!(out, "brake: {} (set_at {}, set_by {})", if brake.released { "released" } else { "held" }, brake.set_at.to_rfc3339(), brake.set_by)?,
None => writeln!(out, "brake: absent")?,
}
}
Err(error) => writeln!(out, "store: undecodable ({error:#})")?,
}
writeln!(out, "effective: {:?}", derive_swarm(&source))?;
Ok(())
}
/// The gate a unit runs before it starts: Ok only when the one derivation
/// (`derive`, the reader behind `read_effective`) says Released. It is a pure
/// read through `FileSource`: it opens no writer and creates no file or
/// directory. Any other reading is an error, which `main` turns into exit 2,
/// and systemd's `ExecCondition` skips the unit on 1-254.
fn check(root: &Path, path: &Path, instance: &Slug, out: &mut impl Write) -> Result<()> {
let effective = derive(&FileSource::new(path), &FileSource::new(swarm_path(root)), instance);
writeln!(out, "effective: {effective:?}")?;
match effective {
Effective::Released(_) => Ok(()),
Effective::Held { reason } => bail!("held: {reason:?}"),
}
}
fn show(root: &Path, instance: &Slug, out: &mut impl Write) -> Result<()> {
let source = FileSource::new(control_path(root, instance)?);
writeln!(out, "instance: {}", instance.0)?;
match load_state(&source) {
Ok(state) => {
match &state.brake {
Some(brake) => writeln!(
out,
"brake: {} (set_at {}, set_by {})",
if brake.released { "released" } else { "held" },
brake.set_at.to_rfc3339(),
brake.set_by
)?,
None => writeln!(out, "brake: absent")?,
}
}
Err(error) => writeln!(out, "store: undecodable ({error:#})")?,
}
writeln!(out, "effective: {:?}", derive(&source, &FileSource::new(swarm_path(root)), instance))?;
Ok(())
}