mur_common/commander.rs
1use serde::{Deserialize, Serialize};
2
3/// Key under which a governance directive is placed in a `ChannelEvent.payload`.
4pub const COMMANDER_DIRECTIVE_KEY: &str = "commander_directive";
5
6/// A cryptographically-signed instruction from the Commander governance plane.
7///
8/// Serialised as a JSON object and placed under [`COMMANDER_DIRECTIVE_KEY`]
9/// in a `ChannelEvent.payload` appended to the `fleet-<name>` channel. The
10/// signing layer lives in `mur-core`; this crate only owns the payload shape.
11/// Cross-network A2A-envelope delivery is Phase 2.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct CommanderDirective {
14 /// Directive kind, e.g. `"kill"`, `"budget_ceiling"`.
15 pub kind: String,
16
17 /// Target fleet name.
18 pub fleet: String,
19
20 /// Optional budget ceiling in USD; `>=0` is honoured, `<0`/`NaN` ignored.
21 pub budget_usd: Option<f64>,
22
23 /// Replay-prevention nonce (UUID v4 recommended).
24 pub nonce: String,
25
26 /// Wall-clock milliseconds since Unix epoch when the directive was issued.
27 pub issued_at_ms: u64,
28}
29
30/// Derived governance state for a running fleet loop.
31///
32/// Built by reducing the stream of [`CommanderDirective`]s received so far.
33#[derive(Debug, Clone, Default, PartialEq)]
34pub struct GovernanceState {
35 /// If `true`, the loop must stop immediately.
36 pub killed: bool,
37
38 /// Active budget ceiling (USD), if any has been applied.
39 pub budget_ceiling: Option<f64>,
40
41 /// Nonce of the directive that produced `killed == true` (audit binding).
42 /// `None` when not killed, or after a resume clears the kill.
43 pub kill_nonce: Option<String>,
44
45 /// Nonce of the directive that set `budget_ceiling` (audit binding).
46 pub budget_nonce: Option<String>,
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn directive_roundtrips_under_the_marker_key() {
55 let d = CommanderDirective {
56 kind: "kill".into(),
57 fleet: "dev".into(),
58 budget_usd: None,
59 nonce: "n1".into(),
60 issued_at_ms: 1_750_000_000_000,
61 };
62 let wrapped = serde_json::json!({ COMMANDER_DIRECTIVE_KEY: &d });
63 let got: CommanderDirective =
64 serde_json::from_value(wrapped[COMMANDER_DIRECTIVE_KEY].clone()).unwrap();
65 assert_eq!(got.kind, "kill");
66 assert_eq!(got.fleet, "dev");
67 assert_eq!(got.issued_at_ms, 1_750_000_000_000);
68 }
69
70 #[test]
71 fn governance_state_default_is_inert() {
72 let g = GovernanceState::default();
73 assert!(!g.killed && g.budget_ceiling.is_none());
74 }
75}