clawft_kernel/workload_ctl/
host_licence.rs1use serde_json::json;
27
28use super::cog_kind::arch_of;
29use super::host_service::{WorkloadHostService, refuse, runtime_refusal};
30use super::msg::{CtlRequest, Refusal, RefusalCode};
31use crate::licence::{CognitumRunGate, RunRequest, RunVerdict, sha256_hex};
32use crate::mesh_swarm_state::GrantOrigin;
33use crate::workload_pkg::VerifiedPackage;
34use crate::workload_pkg::codec::hex_encode;
35use crate::workload_runtime::{InstanceHandle, VerifiedWorkload, WorkloadHost, WorkloadSource};
36
37pub const EVENT_KIND_RUN_PERMIT: &str = "cog.run.permit";
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct LicensedRun {
43 pub cog_id: String,
45 pub version: String,
47 pub arch: String,
49}
50
51fn hash(bytes: &[u8]) -> (String, String) {
52 (sha256_hex(bytes), hex_encode(blake3::hash(bytes).as_bytes()))
53}
54
55fn binaries(w: &VerifiedWorkload) -> Vec<(String, String, String)> {
57 let WorkloadSource::SignedPackage(p) = &w.source else { return Vec::new() };
58 p.binaries
59 .iter()
60 .map(|(arch, b)| {
61 let (s, b3) = hash(&b.bytes);
62 (arch.clone(), s, b3)
63 })
64 .collect()
65}
66
67fn gate_refusal(code: &str, why: String) -> Refusal {
68 refuse(RefusalCode::Governance, format!("licence run gate: [{code}] {why}"))
69}
70
71impl WorkloadHostService {
72 pub fn set_licence_gate(&self, gate: std::sync::Arc<dyn CognitumRunGate>) -> bool {
74 self.licence_gate.set(gate).is_ok()
75 }
76
77 pub fn licence_gate(&self) -> Option<&std::sync::Arc<dyn CognitumRunGate>> {
79 self.licence_gate.get()
80 }
81
82 fn licence_decide(
84 &self,
85 gate: &dyn CognitumRunGate,
86 run: &LicensedRun,
87 arch: &str,
88 (sha256, blake3): (&str, &str),
89 record: Option<(&str, &CtlRequest)>,
90 ) -> Result<(), Refusal> {
91 let r = RunRequest { cog_id: &run.cog_id, version: &run.version, sha256, blake3 };
92 match gate.check(&r) {
93 Ok(RunVerdict::NotSeedBound) => Ok(()),
94 Ok(RunVerdict::Permit(p)) => {
95 if let Some((phase, req)) = record {
96 self.record(
97 EVENT_KIND_RUN_PERMIT,
98 json!({
99 "node": self.node_id(), "phase": phase, "requester": req.requester,
100 "decision_id": req.decision_id, "cog_id": run.cog_id, "version": run.version,
101 "arch": arch, "sha256": sha256, "blake3": p.blake3,
102 "grant_id": p.grant_id, "approval_id": p.approval_id,
103 }),
104 );
105 }
106 Ok(())
107 }
108 Err(e) => Err(gate_refusal(
109 e.code(),
110 format!(
111 "{e} ({} {} {arch}, sha256 {}); remedy: {}",
112 run.cog_id,
113 run.version,
114 &sha256[..16],
115 e.remedy(&run.cog_id, &run.version)
116 ),
117 )),
118 }
119 }
120
121 pub(super) async fn licence_check_place(
124 &self,
125 verified: &VerifiedPackage,
126 w: &VerifiedWorkload,
127 variant: &str,
128 host: &WorkloadHost,
129 req: &CtlRequest,
130 ) -> Result<Option<LicensedRun>, Refusal> {
131 let Some(gate) = self.licence_gate.get() else { return Ok(None) };
132 let bins = binaries(w);
133 let (cog_id, version) = match crate::mesh_artifact_pkg::grant_origin(verified) {
134 GrantOrigin::Cognitum { cog_id, version } => (cog_id, version),
135 _ if bins.iter().any(|(_, s, b)| gate.claims(s, b)) => (w.id.clone(), w.version.clone()),
136 _ => return Ok(None),
137 };
138 let adm = host.runtime().admit(w).await.map_err(|e| runtime_refusal(&e))?;
140 if adm.arch != arch_of(variant) {
141 return Err(gate_refusal(
142 "arch_mismatch",
143 format!("this node's runtime would run the {} binary, the placement chose {variant}", adm.arch),
144 ));
145 }
146 let run = LicensedRun { cog_id, version, arch: adm.arch };
147 let (runs, others): (Vec<_>, Vec<_>) = bins.iter().partition(|(a, _, _)| *a == run.arch);
151 if let Some((arch, s, _)) = others.iter().find(|(_, _, b)| gate.revoked(b)) {
152 return Err(gate_refusal(
153 "hash_revoked",
154 format!("the package's {arch} binary (sha256 {}) is revoked", &s[..16]),
155 ));
156 }
157 let (_, s, b) = runs
158 .first()
159 .ok_or_else(|| gate_refusal("no_binary", format!("the package has no {} binary", run.arch)))?;
160 self.licence_decide(gate.as_ref(), &run, &run.arch, (s, b), Some(("place", req)))?;
161 Ok(Some(run))
162 }
163
164 pub(super) async fn licence_check_start(
168 &self,
169 run: &LicensedRun,
170 host: &WorkloadHost,
171 h: &InstanceHandle,
172 req: &CtlRequest,
173 ) -> Result<(), Refusal> {
174 let Some(gate) = self.licence_gate.get() else { return Ok(()) };
175 let hashes = match host.runtime().staged_payload(h).await {
176 Some(path) => {
177 let bytes = tokio::task::spawn_blocking(move || std::fs::read(path))
178 .await
179 .map_err(|e| refuse(RefusalCode::Runtime, e.to_string()))?
180 .map_err(|e| gate_refusal("staged_unreadable", format!("the staged binary cannot be read: {e}")))?;
181 hash(&bytes)
182 }
183 None => {
184 let w = host
185 .workload_for(h)
186 .await
187 .ok_or_else(|| refuse(RefusalCode::UnknownInstance, "instance has no loaded workload"))?;
188 let (_, s, b) = binaries(&w)
189 .into_iter()
190 .find(|(a, _, _)| *a == run.arch)
191 .ok_or_else(|| gate_refusal("no_binary", format!("the package has no {} binary", run.arch)))?;
192 (s, b)
193 }
194 };
195 self.licence_decide(gate.as_ref(), run, &run.arch, (&hashes.0, &hashes.1), Some(("start", req)))
196 }
197}