Skip to main content

clawft_kernel/workload_ctl/
plane_peers.rs

1//! Operator-assigned trust for `workload-host` targets (ADR-099 section 3).
2//!
3//! The operator's peer list is authoritative on every sync: a listed peer
4//! gets the listed tier (raised or lowered), and a known remote target
5//! that is no longer listed is demoted to `discovered`, which governance
6//! never places on. Demoted targets stay known so their placed instances
7//! can still be stopped and unloaded. Addresses in `keep` (this node's own
8//! in-process host, Seeds) are not touched.
9//!
10//! Trust is bound to a node key, not to an address. A listed peer with a
11//! `key` gets its tier only for that key: a known target at the address
12//! with another key is demoted, and the pinned key is learned by a
13//! `describe` that refuses any other signer. A listed peer without a key
14//! gives its tier to the key first seen at the address; a different key
15//! answering there later is never learned under it (`refresh` requires
16//! the stored key).
17
18use clawft_types::placement::TrustTier as FactsTier;
19
20use super::plane::{PlacementControlPlane, PlaneError, TargetInfo};
21use crate::workload_pkg::codec::hex_decode_exact;
22
23/// One operator-listed `workload-host`.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct OperatorPeer {
26    /// Where it listens.
27    pub addr: String,
28    /// Operator-assigned tier.
29    pub tier: FactsTier,
30    /// The node key the tier is for; `None` trusts the key first seen at
31    /// `addr`.
32    pub key: Option<[u8; 32]>,
33}
34
35impl OperatorPeer {
36    /// A peer trusted on first use.
37    pub fn new(addr: impl Into<String>, tier: FactsTier) -> Self {
38        Self {
39            addr: addr.into(),
40            tier,
41            key: None,
42        }
43    }
44
45    /// Only the node with `key` gets the tier.
46    pub fn with_key(mut self, key: [u8; 32]) -> Self {
47        self.key = Some(key);
48        self
49    }
50
51    /// Whether this entry covers the known target `t` (`known` is every
52    /// known target): a pinned entry covers its key at its address; an
53    /// unpinned one only the first key learned at its address.
54    fn covers(&self, t: &TargetInfo, known: &[TargetInfo]) -> bool {
55        if self.addr != t.addr {
56            return false;
57        }
58        match self.key {
59            Some(k) => hex_decode_exact::<32>(&t.public_key) == Some(k),
60            None => known
61                .iter()
62                .filter(|o| o.addr == self.addr)
63                .min_by_key(|o| (o.learned_ms, o.node_id.clone()))
64                .is_some_and(|first| first.node_id == t.node_id),
65        }
66    }
67}
68
69impl PlacementControlPlane {
70    /// Set the operator-assigned tier of a known target (targets map and
71    /// facts cache). False if the node is not known.
72    pub fn set_tier(&self, node_id: &str, tier: FactsTier) -> bool {
73        let known = self
74            .targets
75            .write()
76            .ok()
77            .and_then(|mut t| t.get_mut(node_id).map(|e| e.tier = tier))
78            .is_some();
79        self.facts.set_trust_tier(node_id, tier);
80        // A Seed is addressed in-process: governance reads the tier held
81        // beside its adapter.
82        let seed = self
83            .seeds
84            .write()
85            .ok()
86            .and_then(|mut s| {
87                s.get_mut(node_id).map(|e| {
88                    e.1 = tier;
89                    // The host's own gate calls read this live.
90                    e.0.set_node_tier(super::facts::governance_tier(tier));
91                })
92            })
93            .is_some();
94        self.persist();
95        known || seed
96    }
97
98    /// Make the known targets match the operator's `peers`. Returns one
99    /// error per peer that could not be learned (it is retried on the next
100    /// sync).
101    pub async fn apply_operator_peers(
102        &self,
103        peers: &[OperatorPeer],
104        keep: &[&str],
105    ) -> Vec<(String, PlaneError)> {
106        let known = self.targets();
107        for t in &known {
108            let want = match peers.iter().find(|p| p.covers(t, &known)) {
109                Some(p) => p.tier,
110                None if keep.contains(&t.addr.as_str()) => continue,
111                None => FactsTier::Discovered,
112            };
113            if t.tier != want {
114                tracing::info!(node = %t.node_id, addr = %t.addr, from = ?t.tier, to = ?want,
115                    "workload peer tier changed by operator policy");
116                self.set_tier(&t.node_id, want);
117            }
118        }
119        let mut failed = Vec::new();
120        for p in peers {
121            // Satisfied by the first key learned at the address (unpinned)
122            // or by the pinned key; otherwise learn it (a pinned key only).
123            if known.iter().any(|t| p.covers(t, &known)) {
124                continue;
125            }
126            if let Err(e) = self.add_target_expecting(&p.addr, p.tier, p.key).await {
127                failed.push((p.addr.clone(), e));
128            }
129        }
130        failed
131    }
132}