1use std::net::SocketAddr;
12use std::sync::Arc;
13use std::time::Duration;
14
15use serde::{Deserialize, Serialize};
16use serde_json::{Value, json};
17
18use crate::chain;
19use crate::gate::GateDecision;
20use crate::workload_pkg::codec::hex_encode;
21use crate::workload_runtime::{
22 HostContract, InstanceHandle, RuntimeError, VerifiedWorkload, WorkloadConfig, WorkloadHost,
23};
24
25use clawft_types::placement::TrustTier;
26
27use super::facts::governance_tier;
28use super::host_service::CtlConfig;
29use super::msg::method;
30use super::plane::{PLANE_CHAIN_SOURCE, PlacementControlPlane, PlacementRecord, PlaneError};
31
32pub const SEED_ROUTE: &str = "remote.api";
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
39pub(super) struct SeedEntry {
40 pub handle: InstanceHandle,
41 pub registry: String,
42 pub version: String,
43 #[serde(default)]
44 pub sha256: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct StorePinOrder {
50 pub node_id: String,
52 pub registry: String,
54 pub id: String,
56 pub version: String,
58 #[serde(default)]
60 pub sha256: Option<String>,
61 pub config: CtlConfig,
63 #[serde(default)]
65 pub start: bool,
66}
67
68impl PlacementControlPlane {
69 pub fn add_seed(
72 &self,
73 node_id: &str,
74 host: Arc<WorkloadHost>,
75 tier: TrustTier,
76 ) -> Result<(), PlaneError> {
77 if !crate::workload_pkg::manifest::valid_token(node_id, 64) {
78 return Err(PlaneError::Invalid(
79 "seed node id must be a plain token".into(),
80 ));
81 }
82 host.runtime()
85 .link_security()
86 .require_pinned(node_id)
87 .map_err(PlaneError::Invalid)?;
88 host.set_node_tier(governance_tier(tier));
89 if let Ok(mut s) = self.seeds.write() {
90 s.insert(node_id.to_string(), (host, tier));
91 }
92 Ok(())
93 }
94
95 fn seed(&self, node_id: &str) -> Option<(Arc<WorkloadHost>, TrustTier)> {
96 self.seeds.read().ok().and_then(|s| s.get(node_id).cloned())
97 }
98
99 fn chain_here(&self, kind: &str, payload: Value) -> String {
100 hex_encode(
101 &self
102 .chain
103 .append(PLANE_CHAIN_SOURCE, kind, Some(payload))
104 .hash,
105 )
106 }
107
108 pub async fn place_store_pin(&self, o: &StorePinOrder) -> Result<PlacementRecord, PlaneError> {
111 let (host, tier) = self
112 .seed(&o.node_id)
113 .ok_or_else(|| PlaneError::Unknown(format!("no Seed adapter for {}", o.node_id)))?;
114 let w = VerifiedWorkload::store_pin(&o.registry, &o.id, &o.version, o.sha256.as_deref())
115 .map_err(|e| PlaneError::Invalid(e.to_string()))?;
116 let ctx = json!({ "node_id": o.node_id, "workload": {
117 "kind": w.kind, "package_trust": "operator_attested", "node_tier": governance_tier(tier),
118 "network": host.runtime().network_exposure(), "secrets": false, "emulated": false,
119 "resource_cost": 0.25, "package_id": format!("store.{}.{}", w.id, w.version),
120 }});
121 let verdict = self.gate.check(&self.node_id, "workload.place", &ctx);
122 let permitted = matches!(verdict, GateDecision::Permit { .. });
123 let decision_id = self.chain_here(
124 chain::EVENT_KIND_WORKLOAD_PLACE,
125 json!({ "phase": "decision", "route": SEED_ROUTE, "runtime": host.runtime().id(),
126 "pin": o.node_id, "store_pin": { "registry": o.registry, "id": o.id,
127 "version": o.version }, "permitted": permitted }),
128 );
129 if !permitted {
130 let reason = match verdict {
131 GateDecision::Deny { reason, .. } | GateDecision::Defer { reason } => reason,
132 GateDecision::Permit { .. } => String::new(),
133 };
134 return Err(PlaneError::Governance(reason));
135 }
136 let cfg = WorkloadConfig {
137 mode: o.config.mode.clone(),
138 args: o.config.args.clone(),
139 host: HostContract::new(SocketAddr::from(([0, 0, 0, 0], o.config.csi_port))),
140 node_id: o.node_id.clone(),
141 };
142 let refuse = |phase: &str, e: &dyn std::fmt::Display| {
143 self.chain_here(
144 chain::EVENT_KIND_WORKLOAD_REFUSE,
145 json!({ "phase": phase, "decision_id": decision_id, "node": o.node_id,
146 "route": SEED_ROUTE, "reason": e.to_string() }),
147 );
148 };
149 let h = match host.load(&w, &cfg).await {
150 Ok(h) => h,
151 Err(e) => {
152 refuse("dispatch", &e);
153 return Err(PlaneError::Call(super::plane::CallFailure::Refused(
154 super::msg::Refusal::new(super::msg::RefusalCode::Admission, e.to_string()),
155 )));
156 }
157 };
158 if o.start
159 && let Err(e) = host.start(&h).await
160 {
161 let rolled = host.unload(h.clone()).await;
164 let why = match &rolled {
165 Ok(()) => format!("start failed: {e}; loaded instance unloaded"),
166 Err(u) => format!("start failed: {e}; unload failed too: {u}"),
167 };
168 refuse("start", &why);
169 if rolled.is_err() {
170 self.track_seed(self.seed_record(o, &w, &h, &decision_id), h, o);
172 }
173 return Err(PlaneError::Call(super::plane::CallFailure::Refused(
174 super::msg::Refusal::new(super::msg::RefusalCode::Runtime, why),
175 )));
176 }
177 let rec = self.seed_record(o, &w, &h, &decision_id);
178 self.chain_here(
179 chain::EVENT_KIND_WORKLOAD_PLACE,
180 json!({ "phase": "placed", "decision_id": decision_id, "node": o.node_id,
181 "route": SEED_ROUTE, "instance_id": h.instance_id }),
182 );
183 self.track_seed(rec.clone(), h, o);
184 Ok(rec)
185 }
186
187 fn seed_record(
188 &self,
189 o: &StorePinOrder,
190 w: &VerifiedWorkload,
191 h: &InstanceHandle,
192 decision_id: &str,
193 ) -> PlacementRecord {
194 PlacementRecord {
195 instance_id: h.instance_id.clone(),
196 node_id: o.node_id.clone(),
197 kind: w.kind.clone(),
198 workload: w.id.clone(),
199 variant: SEED_ROUTE.to_string(),
200 decision_id: decision_id.to_string(),
201 manifest_hash: String::new(),
202 project_id: None,
203 package_id: Some(w.revocation_refs().0),
204 signer_keys: Vec::new(),
205 artifact_hashes: Vec::new(),
206 }
207 }
208
209 fn track_seed(&self, rec: PlacementRecord, h: InstanceHandle, o: &StorePinOrder) {
210 if let Ok(mut m) = self.seed_handles.lock() {
211 m.insert(
212 h.instance_id.clone(),
213 SeedEntry {
214 handle: h,
215 registry: o.registry.clone(),
216 version: o.version.clone(),
217 sha256: o.sha256.clone(),
218 },
219 );
220 }
221 if let Ok(mut p) = self.placements.lock() {
222 p.insert(rec.instance_id.clone(), rec);
223 }
224 self.persist();
225 }
226
227 pub(super) async fn seed_instance(
229 &self,
230 rec: &PlacementRecord,
231 m: &str,
232 ) -> Option<Result<Value, PlaneError>> {
233 let (host, _) = self.seed(&rec.node_id)?;
234 let entry = self
235 .seed_handles
236 .lock()
237 .ok()
238 .and_then(|m| m.get(&rec.instance_id).cloned());
239 let Some(SeedEntry {
240 handle: h,
241 registry,
242 version,
243 sha256,
244 }) = entry
245 else {
246 return Some(Err(PlaneError::Unknown(rec.instance_id.clone())));
247 };
248 let err = |e: crate::workload_runtime::RuntimeError| PlaneError::Governance(e.to_string());
249 let adopted = async {
253 let w = VerifiedWorkload::store_pin(
254 ®istry,
255 &h.workload_id,
256 &version,
257 sha256.as_deref(),
258 )?;
259 host.adopt(&h, &w).await
260 }
261 .await;
262 if let Err(e) = adopted {
263 if m == method::UNLOAD && matches!(e, RuntimeError::NotInstalled(_)) {
266 if let Ok(mut s) = self.seed_handles.lock() {
267 s.remove(&rec.instance_id);
268 }
269 if let Ok(mut p) = self.placements.lock() {
270 p.remove(&rec.instance_id);
271 }
272 self.persist();
273 return Some(Ok(
274 json!({ "unloaded": rec.instance_id, "note": "not on the Seed" }),
275 ));
276 }
277 return Some(Err(err(e)));
278 }
279 Some(match m {
280 method::STATUS => {
281 Ok(json!({ "instance_id": h.instance_id, "status": host.status(&h).await }))
282 }
283 method::START => host
284 .start(&h)
285 .await
286 .map(|_| json!({ "started": h.instance_id }))
287 .map_err(err),
288 method::STOP => host
289 .stop(&h, Duration::from_secs(5))
290 .await
291 .map(|ev| json!({ "stopped": h.instance_id, "evidence": ev.audit() }))
292 .map_err(err),
293 method::UNLOAD => {
294 let r = host.unload(h.clone()).await;
295 if r.is_ok() {
296 if let Ok(mut m) = self.seed_handles.lock() {
297 m.remove(&rec.instance_id);
298 }
299 if let Ok(mut p) = self.placements.lock() {
300 p.remove(&rec.instance_id);
301 }
302 self.persist();
303 }
304 r.map(|_| json!({ "unloaded": rec.instance_id }))
305 .map_err(err)
306 }
307 other => Err(PlaneError::Invalid(format!(
308 "{other} is not supported on a Seed"
309 ))),
310 })
311 }
312}