Skip to main content

clawft_kernel/workload_ctl/
host_licence.rs

1//! The ADR-106 run gate in a `workload-host` (phase 3).
2//!
3//! A package is Cognitum-origin when its signed manifest says so
4//! (`GrantOrigin::Cognitum`) or when any of its binaries is bytes the gate
5//! knows as Cognitum bytes (listed by a held grant, or a revoked artifact
6//! hash): a re-pack under another id does not escape the gate.
7//!
8//! For such a package the host asks the node's [`CognitumRunGate`]:
9//!
10//! - **at `place` / `load`**, for the binary the runtime's own admission
11//!   picks (native: the host arch; container: its arch order), hashes
12//!   computed from its bytes. Only that binary is staged; the package's other
13//!   arches need no grant, but a revoked one refuses the package. The
14//!   placement is refused when the admitted arch is not the chosen variant's;
15//! - **at every `start`**, for the binary that runs, rehashed from the file
16//!   it was staged to when the adapter runs it from disk (native), else from
17//!   the loaded bytes (a container image is tagged by the binary's BLAKE3).
18//!
19//! In a Seed-bound mesh that needs a valid checkout grant and an operator
20//! hash approval. A refusal is a governance refusal whose reason carries the
21//! stable code (`licence run gate: [no_approval] ...`), chained as
22//! `workload.refuse` and shown by `weaver workload place --explain`. A permit
23//! is chained as `cog.run.permit` with the grant and approval ids. Other
24//! signed packages never reach the gate.
25
26use 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
37/// Chain kind of a permitted Cognitum run.
38pub const EVENT_KIND_RUN_PERMIT: &str = "cog.run.permit";
39
40/// What a placed Cognitum cog is, for the check at each start.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct LicensedRun {
43    /// Cog id.
44    pub cog_id: String,
45    /// Version.
46    pub version: String,
47    /// The arch of the binary that runs (from the runtime's admission).
48    pub arch: String,
49}
50
51fn hash(bytes: &[u8]) -> (String, String) {
52    (sha256_hex(bytes), hex_encode(blake3::hash(bytes).as_bytes()))
53}
54
55/// (arch, sha256, blake3) of every binary, computed from its bytes.
56fn 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    /// Use `gate` for Cognitum-origin cogs (first call wins).
73    pub fn set_licence_gate(&self, gate: std::sync::Arc<dyn CognitumRunGate>) -> bool {
74        self.licence_gate.set(gate).is_ok()
75    }
76
77    /// The licence run gate in use, if one was set.
78    pub fn licence_gate(&self) -> Option<&std::sync::Arc<dyn CognitumRunGate>> {
79        self.licence_gate.get()
80    }
81
82    /// Ask the gate for one binary; `record` chains a permit.
83    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    /// The gate at `place` / `load`. `Ok(None)`: not a Cognitum-origin cog
122    /// (or no gate on this node), the gate does not apply.
123    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        // The arch the runtime will actually run, not the one the controller assumed.
139        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        // Only the admitted binary is staged and run, so only it needs a grant
148        // and an approval; the package's other arches are refused only when
149        // their hash is revoked.
150        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    /// The gate again at `start` (a lapse refuses a restart; a running
165    /// instance is left alone). The binary is rehashed from the staged file
166    /// when there is one.
167    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}