Skip to main content

clawft_kernel/workload_ctl/
plane_node_admin.rs

1//! Controller side of the node-admin methods (`dashboard.status`,
2//! `dashboard.token.rotate`): one signed call to a known peer, with the
3//! request and its outcome chained on this node.
4//!
5//! A state-changing method (`dashboard.token.rotate`) is sent only to an
6//! operator-pinned peer: the target's tier comes from `workload-peers.json`
7//! (never from the peer), so a node that is merely discovered or paired cannot
8//! be told to rotate its credentials.
9
10use serde_json::{Value, json};
11
12use clawft_types::placement::TrustTier;
13
14use super::msg::method;
15use super::plane::{PlacementControlPlane, PlaneError};
16
17/// Chain kind of a node-admin request sent from this controller.
18pub const EVENT_NODE_ADMIN_SENT: &str = "node_admin.sent";
19/// Chain kind of its outcome.
20pub const EVENT_NODE_ADMIN_OUTCOME: &str = "node_admin.outcome";
21
22impl PlacementControlPlane {
23    /// Send node-admin method `m` to `node_id` and return its result.
24    pub async fn node_admin(&self, node_id: &str, m: &str, body: Value) -> Result<Value, PlaneError> {
25        if !method::is_node_admin(m) {
26            return Err(PlaneError::Invalid(format!("{m} is not a node-admin method")));
27        }
28        let (target, _) = self.target(node_id)?;
29        if method::mutates(m) && target.tier < TrustTier::Pinned {
30            return Err(PlaneError::Governance(format!(
31                "{m} needs an operator-pinned peer; {node_id} is {:?} (set its tier to pinned in workload-peers.json)",
32                target.tier
33            )));
34        }
35        self.ensure_described(node_id).await;
36        let decision_id = method::mutates(m).then(|| {
37            self.chain_event(EVENT_NODE_ADMIN_SENT, json!({ "node": node_id, "method": m }))
38        });
39        let out = self.call(node_id, m, decision_id.clone(), body).await;
40        self.chain_event(
41            EVENT_NODE_ADMIN_OUTCOME,
42            json!({ "node": node_id, "method": m, "decision_id": decision_id,
43                    "ok": out.is_ok(), "error": out.as_ref().err().map(|e| e.to_string()) }),
44        );
45        out
46    }
47}