mind-body: the operator's brake and burn-rate dial, the reader and the mind-control CLI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-09 15:58:29 +02:00
commit 4b3e850fef
9 changed files with 971 additions and 4 deletions

166
Cargo.lock generated
View file

@ -63,6 +63,12 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "async-trait"
version = "0.1.92"
@ -113,6 +119,30 @@ dependencies = [
"generic-array",
]
[[package]]
name = "borsh"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a"
dependencies = [
"borsh-derive",
"bytes",
"cfg_aliases",
]
[[package]]
name = "borsh-derive"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56"
dependencies = [
"once_cell",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@ -141,6 +171,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chrono"
version = "0.4.45"
@ -246,7 +282,7 @@ dependencies = [
"cultcache-rs",
"ed25519-dalek",
"hmac",
"rand",
"rand 0.9.5",
"reed-solomon-erasure",
"rmp-serde",
"rmpv",
@ -372,6 +408,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
@ -592,6 +634,12 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hmac"
version = "0.12.1"
@ -683,6 +731,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]]
name = "inout"
version = "0.1.4"
@ -757,7 +815,7 @@ version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a"
dependencies = [
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
@ -766,6 +824,19 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "mind-body"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"cultcache-rs",
"eureka-pipeline",
"rust_decimal",
"serde",
"tempfile",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -861,6 +932,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
@ -891,16 +971,37 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
@ -1044,6 +1145,23 @@ dependencies = [
"serde_bytes",
]
[[package]]
name = "rust_decimal"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a"
dependencies = [
"arrayvec",
"borsh",
"bytes",
"num-traits",
"rand 0.8.8",
"rand 0.9.5",
"serde",
"serde_json",
"wasm-bindgen",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
@ -1299,7 +1417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@ -1361,6 +1479,36 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml_datetime"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.17+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.1.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
dependencies = [
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.44"
@ -1486,6 +1634,7 @@ dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"serde",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
@ -1694,6 +1843,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"

View file

@ -4,6 +4,7 @@ members = [
"crates/huginn-mind",
"crates/huginn-daemon",
"crates/eureka-state",
"crates/mind-body",
]
resolver = "3"

View file

@ -0,0 +1,34 @@
[package]
name = "mind-body"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish.workspace = true
# One crate for her organs (rationale "One crate for her organs"). It holds the
# operator's brake and burn-rate dial today: a root-owned per-instance CultCache
# store (`control`), the operator's CLI over it (`cli`, the `mind-control`
# binary) and the one fail-closed reader every acting organ calls
# (`control::read_effective`). The waker joins this crate in cut mind-wake.
#
# `rust_decimal` is the dial's number type: heat, cadence and the run cap are
# money-adjacent and the tests pin exact products, which a float would blur. It
# serialises as a string, so the stored document carries no float.
[dependencies]
anyhow = "1"
chrono = { version = "0.4.44", features = ["serde"] }
cultcache-rs.workspace = true
eureka-pipeline = { path = "../eureka-pipeline" }
rust_decimal = { version = "1", default-features = false, features = ["std", "serde"] }
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
tempfile = "3"
[lib]
name = "mind_body"
path = "src/lib.rs"
[[bin]]
name = "mind-control"
path = "src/main.rs"

117
crates/mind-body/src/cli.rs Normal file
View file

@ -0,0 +1,117 @@
//! The `mind-control` command line: the operator's hand on one instance's
//! control store. It performs no permission check of its own: the file is
//! root-owned and the filesystem is the authority.
//!
//! ```text
//! mind-control --instance <slug> show
//! mind-control --instance <slug> brake hold
//! mind-control --instance <slug> brake release
//! mind-control --instance <slug> dial set --heat H --base-cooldown-s S --base-run-usd U
//! ```
use std::io::Write;
use std::path::Path;
use std::str::FromStr;
use anyhow::{Context, Result, anyhow, bail};
use chrono::{DateTime, Utc};
use eureka_pipeline::Slug;
use rust_decimal::Decimal;
use crate::control::{BurnRate, ControlWriter, FileSource, control_path, load_state, read_effective};
const USAGE: &str =
"usage: mind-control --instance <slug> show | brake hold | brake release | dial set --heat H --base-cooldown-s S --base-run-usd U";
/// 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`. The root, the
/// operator's name, the clock and the output are parameters so a test drives
/// the whole command without touching `/etc`.
pub fn run(args: &[String], root: &Path, set_by: &str, now: DateTime<Utc>, out: &mut impl Write) -> Result<()> {
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(&path, &instance, out),
["brake", "hold"] => ControlWriter::open(&path)?.set_brake(false, now, set_by),
["brake", "release"] => ControlWriter::open(&path)?.set_brake(true, now, set_by),
["dial", "set", flags @ ..] => {
let dial = parse_dial(flags, now, set_by)?;
ControlWriter::open(&path)?.set_dial(dial)
}
_ => bail!("{USAGE}"),
}
}
fn parse_dial(flags: &[&str], now: DateTime<Utc>, set_by: &str) -> Result<BurnRate> {
let (mut heat, mut cooldown, mut run_usd) = (None, None, None);
let mut rest = flags.iter();
while let Some(flag) = rest.next() {
let value = rest.next().ok_or_else(|| anyhow!("{flag} needs a value"))?;
let slot = match *flag {
"--heat" => &mut heat,
"--base-cooldown-s" => &mut cooldown,
"--base-run-usd" => &mut run_usd,
other => bail!("unknown flag {other:?}"),
};
if slot.replace(*value).is_some() {
bail!("{flag} given twice");
}
}
let need = |value: Option<&str>, name: &str| value.ok_or_else(|| anyhow!("{name} is required"));
Ok(BurnRate {
heat: Decimal::from_str(need(heat, "--heat")?).context("--heat is not a decimal")?,
base_cooldown_s: need(cooldown, "--base-cooldown-s")?
.parse()
.context("--base-cooldown-s is not a whole number of seconds")?,
base_run_usd: Decimal::from_str(need(run_usd, "--base-run-usd")?).context("--base-run-usd is not a decimal")?,
set_at: now,
set_by: set_by.to_string(),
})
}
fn show(path: &Path, instance: &Slug, out: &mut impl Write) -> Result<()> {
let source = FileSource::new(path);
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")?,
}
match &state.dial {
Some(dial) => writeln!(
out,
"dial: heat {} base_cooldown_s {} base_run_usd {} (set_at {}, set_by {})",
dial.heat,
dial.base_cooldown_s,
dial.base_run_usd,
dial.set_at.to_rfc3339(),
dial.set_by
)?,
None => writeln!(out, "dial: absent")?,
}
}
Err(error) => writeln!(out, "store: undecodable ({error:#})")?,
}
writeln!(out, "effective: {:?}", read_effective(&source))?;
Ok(())
}

View file

@ -0,0 +1,231 @@
//! The operator's brake and burn-rate dial, and the one reader of them.
//!
//! State is a CultCache single-file store at
//! `/etc/gamecult/minds/<instance>/control.cc`, root-owned and world-readable.
//! The operator's CLI replaces a document whole; every organ that acts calls
//! `read_effective` before the consequence and takes its answer as given. The
//! cadence and the run cap are derived here and nowhere else.
//!
//! Fail closed: the reader's only route to `Released` is a decodable store
//! holding a released brake and an in-bounds dial. Anything else (no file, no
//! brake, a held brake, no dial, a dial out of bounds, bytes that do not
//! decode, a file that cannot be read) is `Held`, with the reason.
//!
//! The reader opens the file read-only through CultCache's read-only snapshot
//! door: it takes no lock and creates no sibling file, so the units she runs as
//! need no write authority anywhere under `/etc`. Writing is the operator's;
//! the filesystem is the authority and nothing here checks permission.
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry, SingleFileMessagePackBackingStore};
use eureka_pipeline::Slug;
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
/// Where every instance's control directory lives. Tests and the library never
/// touch it: the root is a parameter.
pub const CONTROL_ROOT: &str = "/etc/gamecult/minds";
/// The one key each control document has in the store.
pub const BRAKE_KEY: &str = "brake";
pub const DIAL_KEY: &str = "dial";
/// `<root>/<instance>/control.cc`, after the leaf's slug grammar has passed, so
/// an instance name cannot walk out of the root.
pub fn control_path(root: &Path, instance: &Slug) -> Result<PathBuf> {
instance
.validate_slug()
.map_err(|refusal| anyhow::anyhow!("instance {:?} is not a slug: {refusal:?}", instance.0))?;
Ok(root.join(&instance.0).join("control.cc"))
}
/// The brake: released means the operator allows her to act.
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "eureka.control.brake.v1", schema = "ControlBrake")]
pub struct Brake {
#[cultcache(key = 0)]
pub released: bool,
#[cultcache(key = 1)]
pub set_at: DateTime<Utc>,
#[cultcache(key = 2)]
pub set_by: String,
}
/// The burn-rate dial: `heat` scales how active she is; the base values say
/// what heat 1.0 means. Cadence and run cap are derived from it, never stored.
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "eureka.control.burn_rate.v1", schema = "ControlBurnRate")]
pub struct BurnRate {
#[cultcache(key = 0)]
pub heat: Decimal,
#[cultcache(key = 1)]
pub base_cooldown_s: u32,
#[cultcache(key = 2)]
pub base_run_usd: Decimal,
#[cultcache(key = 3)]
pub set_at: DateTime<Utc>,
#[cultcache(key = 4)]
pub set_by: String,
}
/// Heat's inclusive bounds.
pub fn heat_min() -> Decimal {
Decimal::new(5, 2)
}
pub fn heat_max() -> Decimal {
Decimal::new(20, 1)
}
impl BurnRate {
/// The one bounds rule: the reader holds on it and the CLI refuses on it.
pub fn in_bounds(&self) -> bool {
self.heat >= heat_min() && self.heat <= heat_max() && self.base_cooldown_s > 0 && self.base_run_usd > Decimal::ZERO
}
/// Wake cadence: `base_cooldown_s / heat`, to the nanosecond.
fn cadence(&self) -> Option<Duration> {
let seconds = Decimal::from(self.base_cooldown_s).checked_div(self.heat)?;
let whole = seconds.trunc();
let nanos = ((seconds - whole) * Decimal::from(1_000_000_000u32)).round();
Some(Duration::new(whole.to_u64()?, nanos.to_u32()?))
}
/// Per-run cap: `base_run_usd x heat`.
fn run_cap_usd(&self) -> Option<Decimal> {
self.base_run_usd.checked_mul(self.heat)
}
}
/// Why the reader said held.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HeldReason {
BrakeAbsent,
BrakeHeld,
DialAbsent,
DialOutOfBounds,
Undecodable,
}
/// What the operator's store allows right now.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Effective {
Held { reason: HeldReason },
Released { cadence: Duration, run_cap_usd: Decimal },
}
/// The store's contents, decoded and not yet judged.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ControlState {
pub brake: Option<Brake>,
pub dial: Option<BurnRate>,
}
impl ControlState {
/// The one derivation. Order: brake first, then dial; each failing rung is
/// its own reason.
pub fn effective(&self) -> Effective {
let held = |reason| Effective::Held { reason };
let Some(brake) = &self.brake else { return held(HeldReason::BrakeAbsent) };
if !brake.released {
return held(HeldReason::BrakeHeld);
}
let Some(dial) = &self.dial else { return held(HeldReason::DialAbsent) };
if !dial.in_bounds() {
return held(HeldReason::DialOutOfBounds);
}
match (dial.cadence(), dial.run_cap_usd()) {
(Some(cadence), Some(run_cap_usd)) => Effective::Released { cadence, run_cap_usd },
_ => held(HeldReason::DialOutOfBounds),
}
}
}
/// The reader's port: the store's envelopes as one snapshot. An absent store is
/// an empty snapshot; an unreadable one is an error.
pub trait ControlSource {
fn snapshot(&self) -> Result<Vec<CultCacheEnvelope>>;
}
/// The real source: the control file, read without locks and without writes.
pub struct FileSource {
path: PathBuf,
}
impl FileSource {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
}
impl ControlSource for FileSource {
fn snapshot(&self) -> Result<Vec<CultCacheEnvelope>> {
SingleFileMessagePackBackingStore::new(&self.path).pull_all_read_only_snapshot()
}
}
fn registered_cache() -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<Brake>()?;
cache.register_entry_type::<BurnRate>()?;
Ok(cache)
}
/// Decodes the store into its two documents. Any envelope the cache does not
/// know, or any payload that does not decode, is an error: a store the reader
/// cannot fully read is not one it may act on.
pub fn load_state(source: &impl ControlSource) -> Result<ControlState> {
let mut cache = registered_cache()?;
for envelope in source.snapshot().context("control store cannot be read")? {
cache.put_raw_envelope(envelope)?;
}
Ok(ControlState { brake: cache.get::<Brake>(BRAKE_KEY)?, dial: cache.get::<BurnRate>(DIAL_KEY)? })
}
/// The only reader every organ uses (the waker now; the permit issuer and the
/// Persona organ later).
pub fn read_effective(source: &impl ControlSource) -> Effective {
match load_state(source) {
Ok(state) => state.effective(),
Err(_) => Effective::Held { reason: HeldReason::Undecodable },
}
}
/// The operator's writer over one control file. Each setter replaces its
/// document whole; the other document is untouched. The file is created on the
/// first write.
pub struct ControlWriter {
cache: CultCache,
}
impl ControlWriter {
pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
let mut cache = registered_cache()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path))?;
cache.pull_all_backing_stores()?;
Ok(Self { cache })
}
pub fn set_brake(&mut self, released: bool, set_at: DateTime<Utc>, set_by: &str) -> Result<()> {
let brake = Brake { released, set_at, set_by: set_by.to_string() };
self.cache.put(BRAKE_KEY, &brake)?;
Ok(())
}
/// Refuses an out-of-bounds dial before anything is written, so the store
/// never holds one that the CLI put there.
pub fn set_dial(&mut self, dial: BurnRate) -> Result<()> {
if !dial.in_bounds() {
anyhow::bail!(
"dial refused: heat must be within {}..={}, base_cooldown_s above 0 and base_run_usd above 0",
heat_min(),
heat_max()
);
}
self.cache.put(DIAL_KEY, &dial)?;
Ok(())
}
}

View file

@ -0,0 +1,9 @@
//! `mind-body`: the organs of one Mind's body that live in Huginn's workspace.
//!
//! Today that is `control`, the operator's brake and burn-rate dial, and `cli`,
//! the operator's hand on it. The brake and the dial are the operator's consent
//! (ruling `eureka-body:ruling:brake-dial-root-store-now`), not her body: the
//! store is root-owned and no organ of hers writes it.
pub mod cli;
pub mod control;

View file

@ -0,0 +1,21 @@
//! `mind-control`: the operator's hand on a Mind's brake and burn-rate dial.
//! All behaviour is `mind_body::cli`; this reads the process's environment and
//! clock, the two things the library never reads.
use std::path::Path;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let set_by = mind_body::cli::operator_name(std::env::var("SUDO_USER").ok(), std::env::var("USER").ok());
let result = mind_body::cli::run(
&args,
Path::new(mind_body::control::CONTROL_ROOT),
&set_by,
chrono::Utc::now(),
&mut std::io::stdout(),
);
if let Err(error) = result {
eprintln!("mind-control: {error:#}");
std::process::exit(2);
}
}

View file

@ -0,0 +1,101 @@
//! The `mind-control` command driven end to end through `mind_body::cli::run`
//! against a temporary root, and the binary once as a process for its exit
//! status. Nothing touches `/etc`.
use chrono::{TimeZone, Utc};
use mind_body::cli::{operator_name, run};
fn go(root: &std::path::Path, args: &[&str], set_by: &str, secs: i64) -> (anyhow::Result<()>, String) {
let args: Vec<String> = args.iter().map(|a| a.to_string()).collect();
let mut out = Vec::new();
let now = Utc.timestamp_opt(1_800_000_000 + secs, 0).unwrap();
let result = run(&args, root, set_by, now, &mut out);
(result, String::from_utf8(out).unwrap())
}
#[test]
fn show_reports_every_rung_from_an_absent_store_to_released() {
let root = tempfile::tempdir().unwrap();
let (r, out) = go(root.path(), &["--instance", "eureka", "show"], "op", 0);
r.unwrap();
assert!(out.contains("brake: absent") && out.contains("dial: absent") && out.contains("BrakeAbsent"), "{out}");
go(root.path(), &["--instance", "eureka", "brake", "release"], "alice", 1).0.unwrap();
let (_, out) = go(root.path(), &["--instance", "eureka", "show"], "op", 2);
assert!(out.contains("brake: released") && out.contains("set_by alice") && out.contains("DialAbsent"), "{out}");
go(root.path(), &["--instance", "eureka", "dial", "set", "--heat", "0.5", "--base-cooldown-s", "3600", "--base-run-usd", "5"], "bob", 3)
.0
.unwrap();
let (_, out) = go(root.path(), &["--instance", "eureka", "show"], "op", 4);
assert!(out.contains("heat 0.5") && out.contains("base_cooldown_s 3600") && out.contains("set_by bob"), "{out}");
assert!(out.contains("Released") && out.contains("7200s"), "{out}");
go(root.path(), &["--instance", "eureka", "brake", "hold"], "alice", 5).0.unwrap();
let (_, out) = go(root.path(), &["--instance", "eureka", "show"], "op", 6);
assert!(out.contains("brake: held") && out.contains("BrakeHeld"), "{out}");
}
#[test]
fn instances_do_not_share_a_store() {
let root = tempfile::tempdir().unwrap();
go(root.path(), &["--instance", "one", "brake", "release"], "op", 0).0.unwrap();
let (_, out) = go(root.path(), &["--instance", "two", "show"], "op", 1);
assert!(out.contains("brake: absent"), "{out}");
assert!(root.path().join("one").join("control.cc").exists());
}
#[test]
fn a_dial_set_outside_bounds_refuses_and_leaves_the_store_unchanged() {
let root = tempfile::tempdir().unwrap();
go(root.path(), &["--instance", "eureka", "brake", "release"], "op", 0).0.unwrap();
go(root.path(), &["--instance", "eureka", "dial", "set", "--heat", "1", "--base-cooldown-s", "60", "--base-run-usd", "1"], "op", 1)
.0
.unwrap();
let path = root.path().join("eureka").join("control.cc");
let before = std::fs::read(&path).unwrap();
for heat in ["2.0001", "0.0499", "0", "-1"] {
let (r, _) = go(root.path(), &["--instance", "eureka", "dial", "set", "--heat", heat, "--base-cooldown-s", "60", "--base-run-usd", "1"], "op", 9);
assert!(r.is_err(), "heat {heat} must be refused");
assert_eq!(std::fs::read(&path).unwrap(), before, "heat {heat} changed the store");
}
// A refusal on a store that does not exist yet must not create it.
let fresh = tempfile::tempdir().unwrap();
let (r, _) = go(fresh.path(), &["--instance", "eureka", "dial", "set", "--heat", "5", "--base-cooldown-s", "60", "--base-run-usd", "1"], "op", 0);
assert!(r.is_err());
assert!(!fresh.path().join("eureka").join("control.cc").exists());
}
#[test]
fn malformed_commands_are_refused_and_write_nothing() {
let root = tempfile::tempdir().unwrap();
let bad: &[&[&str]] = &[
&[],
&["show"],
&["--instance", "eureka"],
&["--instance", "eureka", "brake"],
&["--instance", "eureka", "brake", "toggle"],
&["--instance", "eureka", "dial", "set", "--heat", "1"],
&["--instance", "eureka", "dial", "set", "--heat", "1", "--heat", "1", "--base-cooldown-s", "60", "--base-run-usd", "1"],
&["--instance", "eureka", "dial", "set", "--heat", "x", "--base-cooldown-s", "60", "--base-run-usd", "1"],
&["--instance", "eureka", "dial", "set", "--heat", "1", "--base-cooldown-s", "1.5", "--base-run-usd", "1"],
&["--instance", "eureka", "dial", "set", "--heat", "1", "--base-cooldown-s", "60", "--base-run-usd", "1", "--extra", "1"],
&["--instance", "eureka", "dial", "set", "--heat"],
&["--instance", "../evil", "brake", "release"],
];
for args in bad {
assert!(go(root.path(), args, "op", 0).0.is_err(), "{args:?}");
}
assert_eq!(std::fs::read_dir(root.path()).unwrap().count(), 0, "a refused command wrote something");
}
#[test]
fn the_operator_is_sudo_user_then_user_then_unknown() {
let s = |v: &str| Some(v.to_string());
assert_eq!(operator_name(s("alice"), s("root")), "alice");
assert_eq!(operator_name(None, s("bob")), "bob");
assert_eq!(operator_name(s(""), s("bob")), "bob");
assert_eq!(operator_name(None, s("")), "unknown");
assert_eq!(operator_name(None, None), "unknown");
}

View file

@ -0,0 +1,295 @@
//! Behavioural tests for the brake, the dial and the one reader. Every test
//! works in a temporary directory through the same file door the units use;
//! nothing touches `/etc`.
//!
//! The permission boundary (no user she runs as can write the file) belongs to
//! the filesystem, not to this crate, and is not tested here: the verify
//! container runs as root, where a permission failure cannot be provoked.
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Result, anyhow};
use chrono::{DateTime, TimeZone, Utc};
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry, SingleFileMessagePackBackingStore};
use mind_body::control::{
BRAKE_KEY, BurnRate, ControlSource, ControlWriter, Effective, FileSource, HeldReason, control_path, load_state, read_effective,
};
use rust_decimal::Decimal;
use std::str::FromStr;
fn dec(text: &str) -> Decimal {
Decimal::from_str(text).unwrap()
}
fn at(seconds: i64) -> DateTime<Utc> {
Utc.timestamp_opt(1_800_000_000 + seconds, 0).unwrap()
}
fn dial(heat: &str, cooldown: u32, run_usd: &str) -> BurnRate {
BurnRate { heat: dec(heat), base_cooldown_s: cooldown, base_run_usd: dec(run_usd), set_at: at(0), set_by: "op".into() }
}
fn store_path(dir: &Path) -> PathBuf {
dir.join("control.cc")
}
fn held(reason: HeldReason) -> Effective {
Effective::Held { reason }
}
/// A written store: brake released and the given dial, through the real writer.
fn released_store(dir: &Path, dial: BurnRate) -> PathBuf {
let path = store_path(dir);
let mut writer = ControlWriter::open(&path).unwrap();
writer.set_brake(true, at(0), "op").unwrap();
writer.set_dial(dial).unwrap();
path
}
fn effective(path: &Path) -> Effective {
read_effective(&FileSource::new(path))
}
/// A dial written around the writer's bounds check, so the reader's own check
/// is what is under test.
fn write_unchecked_dial(path: &Path, dial: &BurnRate) {
let mut cache = CultCache::new();
cache.register_entry_type::<BurnRate>().unwrap();
cache.register_entry_type::<mind_body::control::Brake>().unwrap();
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path)).unwrap();
cache.pull_all_backing_stores().unwrap();
cache.put(mind_body::control::DIAL_KEY, dial).unwrap();
}
#[test]
fn an_absent_store_reads_as_held() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(effective(&store_path(dir.path())), held(HeldReason::BrakeAbsent));
assert!(!store_path(dir.path()).exists(), "reading must not create the store");
}
#[test]
fn an_absent_brake_reads_as_held_even_with_a_dial() {
let dir = tempfile::tempdir().unwrap();
let path = store_path(dir.path());
ControlWriter::open(&path).unwrap().set_dial(dial("1", 3600, "1")).unwrap();
assert_eq!(effective(&path), held(HeldReason::BrakeAbsent));
}
#[test]
fn an_undecodable_store_reads_as_held() {
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial("1", 3600, "1"));
let good = std::fs::read(&path).unwrap();
// Garbage in place of the store.
std::fs::write(&path, b"this is not a cultcache store").unwrap();
assert_eq!(effective(&path), held(HeldReason::Undecodable));
// A store cut off mid-document.
std::fs::write(&path, &good[..good.len() / 2]).unwrap();
assert_eq!(effective(&path), held(HeldReason::Undecodable));
// The untouched store is still released, so the two reads above were the damage.
std::fs::write(&path, &good).unwrap();
assert!(matches!(effective(&path), Effective::Released { .. }));
}
#[test]
fn a_path_that_cannot_be_read_as_a_file_reads_as_held() {
let dir = tempfile::tempdir().unwrap();
// A directory sits where the store should be: it exists and cannot be read as a file.
let path = store_path(dir.path());
std::fs::create_dir(&path).unwrap();
assert_eq!(effective(&path), held(HeldReason::Undecodable));
}
#[test]
fn a_source_that_fails_reads_as_held() {
struct Failing;
impl ControlSource for Failing {
fn snapshot(&self) -> Result<Vec<CultCacheEnvelope>> {
Err(anyhow!("disk gone"))
}
}
assert_eq!(read_effective(&Failing), held(HeldReason::Undecodable));
}
#[test]
fn an_unknown_document_type_makes_the_store_undecodable() {
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "eureka.control.something_else.v1", schema = "Other")]
struct Other {
#[cultcache(key = 0)]
value: u32,
}
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial("1", 3600, "1"));
let mut cache = CultCache::new();
cache.register_entry_type::<Other>().unwrap();
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
cache.put("x", &Other { value: 1 }).unwrap();
// The store now holds a released brake, a good dial and a foreign document.
assert_eq!(effective(&path), held(HeldReason::Undecodable));
}
#[test]
fn a_brake_payload_that_does_not_decode_makes_the_store_undecodable() {
// Same type id as the brake, a different shape: the payload cannot decode as a Brake.
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "eureka.control.brake.v1", schema = "ControlBrake")]
struct WrongBrake {
#[cultcache(key = 0)]
released: String,
}
let dir = tempfile::tempdir().unwrap();
let path = store_path(dir.path());
let mut cache = CultCache::new();
cache.register_entry_type::<WrongBrake>().unwrap();
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
cache.put(BRAKE_KEY, &WrongBrake { released: "yes".into() }).unwrap();
assert_eq!(effective(&path), held(HeldReason::Undecodable));
}
#[test]
fn a_decode_failure_outranks_a_held_brake() {
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial("1", 3600, "1"));
ControlWriter::open(&path).unwrap().set_brake(false, at(5), "op").unwrap();
assert_eq!(effective(&path), held(HeldReason::BrakeHeld));
std::fs::write(&path, b"junk").unwrap();
assert_eq!(effective(&path), held(HeldReason::Undecodable));
}
#[test]
fn a_released_brake_with_no_dial_reads_held_dial_absent() {
let dir = tempfile::tempdir().unwrap();
let path = store_path(dir.path());
ControlWriter::open(&path).unwrap().set_brake(true, at(0), "op").unwrap();
assert_eq!(effective(&path), held(HeldReason::DialAbsent));
}
#[test]
fn dial_bounds_are_inclusive_at_both_ends() {
for (heat, expected_in) in [("0.05", true), ("2.0", true), ("0.0499", false), ("2.0001", false), ("0", false), ("-1", false), ("3", false)] {
let dir = tempfile::tempdir().unwrap();
let path = released_store_unchecked(dir.path(), dial(heat, 3600, "1"));
let got = effective(&path);
if expected_in {
assert!(matches!(got, Effective::Released { .. }), "heat {heat}: {got:?}");
} else {
assert_eq!(got, held(HeldReason::DialOutOfBounds), "heat {heat}");
}
}
}
#[test]
fn a_zero_base_cooldown_or_a_non_positive_run_cost_is_out_of_bounds() {
for bad in [dial("1", 0, "1"), dial("1", 3600, "0"), dial("1", 3600, "-0.5")] {
let dir = tempfile::tempdir().unwrap();
let path = released_store_unchecked(dir.path(), bad.clone());
assert_eq!(effective(&path), held(HeldReason::DialOutOfBounds), "{bad:?}");
}
let dir = tempfile::tempdir().unwrap();
let smallest = released_store_unchecked(dir.path(), dial("1", 1, "0.000001"));
assert!(matches!(effective(&smallest), Effective::Released { .. }));
}
fn released_store_unchecked(dir: &Path, bad: BurnRate) -> PathBuf {
let path = store_path(dir);
ControlWriter::open(&path).unwrap().set_brake(true, at(0), "op").unwrap();
write_unchecked_dial(&path, &bad);
path
}
#[test]
fn cadence_and_run_cap_are_the_dial_divided_and_multiplied_by_heat() {
for (heat, cooldown, run_usd, cadence, cap) in [
("0.5", 3600u32, "5", Duration::from_secs(7200), dec("2.5")),
("2.0", 3600, "5", Duration::from_secs(1800), dec("10.0")),
("1", 600, "3", Duration::from_secs(600), dec("3")),
("0.05", 90, "4", Duration::from_secs(1800), dec("0.20")),
("1.5", 10, "2", Duration::new(6, 666_666_667), dec("3.0")),
("2.0", 1, "0.25", Duration::from_millis(500), dec("0.500")),
] {
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial(heat, cooldown, run_usd));
assert_eq!(effective(&path), Effective::Released { cadence, run_cap_usd: cap }, "heat {heat} cooldown {cooldown} run {run_usd}");
}
}
#[test]
fn the_operators_last_write_is_what_is_in_force_and_provenance_round_trips() {
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial("1", 3600, "1"));
assert!(matches!(effective(&path), Effective::Released { .. }));
ControlWriter::open(&path).unwrap().set_brake(false, at(60), "alice").unwrap();
assert_eq!(effective(&path), held(HeldReason::BrakeHeld));
let state = load_state(&FileSource::new(&path)).unwrap();
let brake = state.brake.unwrap();
assert_eq!((brake.released, brake.set_at, brake.set_by.as_str()), (false, at(60), "alice"));
// The dial survives the brake write untouched.
assert_eq!(state.dial.unwrap().set_by, "op");
ControlWriter::open(&path).unwrap().set_brake(true, at(120), "bob").unwrap();
assert!(matches!(effective(&path), Effective::Released { .. }));
let brake = load_state(&FileSource::new(&path)).unwrap().brake.unwrap();
assert_eq!((brake.released, brake.set_at, brake.set_by.as_str()), (true, at(120), "bob"));
}
#[test]
fn setting_the_dial_replaces_it_whole_and_keeps_the_brake() {
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial("1", 3600, "1"));
let mut next = dial("0.5", 100, "8");
next.set_at = at(30);
next.set_by = "carol".into();
ControlWriter::open(&path).unwrap().set_dial(next.clone()).unwrap();
let state = load_state(&FileSource::new(&path)).unwrap();
assert_eq!(state.dial, Some(next));
assert!(state.brake.unwrap().released);
}
#[test]
fn the_writer_refuses_an_out_of_bounds_dial_and_leaves_the_store_unchanged() {
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial("1", 3600, "1"));
let before = std::fs::read(&path).unwrap();
for bad in [dial("2.0001", 3600, "1"), dial("0.0499", 3600, "1"), dial("1", 0, "1"), dial("1", 3600, "0")] {
assert!(ControlWriter::open(&path).unwrap().set_dial(bad).is_err());
assert_eq!(std::fs::read(&path).unwrap(), before);
}
}
#[test]
fn reading_writes_nothing_and_takes_no_lock_file() {
let dir = tempfile::tempdir().unwrap();
let path = released_store(dir.path(), dial("1", 3600, "1"));
let listing = |dir: &Path| {
let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name()).collect();
names.sort();
names
};
// The writer left its sibling lock file; remove it, so a reader that wants one has to create it.
for entry in std::fs::read_dir(dir.path()).unwrap() {
let entry = entry.unwrap().path();
if entry != path {
std::fs::remove_file(entry).unwrap();
}
}
let before = (listing(dir.path()), std::fs::read(&path).unwrap());
assert!(matches!(effective(&path), Effective::Released { .. }));
assert_eq!((listing(dir.path()), std::fs::read(&path).unwrap()), before);
}
#[test]
fn the_control_path_is_derived_from_a_valid_instance_only() {
use eureka_pipeline::Slug;
let root = Path::new("/etc/gamecult/minds");
assert_eq!(control_path(root, &Slug("eureka".into())).unwrap(), root.join("eureka").join("control.cc"));
for bad in ["", "../etc", "a/b", "A b", "eureka/.."] {
assert!(control_path(root, &Slug(bad.into())).is_err(), "{bad:?}");
}
}