Skip to main content

clawft_kernel/licence/
run_gate.rs

1//! The run gate as placement uses it (ADR-106 section 4 step 7, phase 3).
2//!
3//! [`check_run`] wraps [`super::may_run`] with the two things placement needs
4//! on top: whether the gate applies at all, and a stable reason for every
5//! refusal (`weaver workload place --explain` shows it, and the host chains
6//! it as `workload.refuse`).
7//!
8//! - **Applies** when this node holds a Seed binding (bound, unbound or
9//!   orphaned), its store is unreadable, the sticky bound marker exists (a
10//!   deleted store file does not reset it), or it holds approvals. A node
11//!   with none of these is not in a Seed-bound mesh, and its Cognitum cogs
12//!   keep the ADR-105 path (operator-signed package) unchanged:
13//!   [`RunVerdict::NotSeedBound`].
14//! - **Refusals**, in this order: no binding in effect (`binding_inactive`),
15//!   no grant held for the cog version (`no_grant`), a grant held but no
16//!   longer valid (`grant_lapsed`: expired, withdrawn, key revoked), a valid
17//!   grant that does not list this binary (`not_in_grant`), the binary's
18//!   BLAKE3 revoked (`hash_revoked`), no operator approval covering its
19//!   sha256 (`no_approval`).
20
21use std::sync::Arc;
22
23use super::gate::{RunPermit, RunRequest};
24use super::{ApprovalStore, CheckoutGrantStore};
25
26/// Why the gate refused.
27#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
28pub enum RunRefusal {
29    /// A binding is held but none is in effect (unbound, orphaned by a mesh
30    /// id change, no local mesh id, or the store is unreadable).
31    #[error("no Seed binding is in effect on this node ({0})")]
32    BindingInactive(String),
33    /// No grant for this cog version was ever received here.
34    #[error("no checkout grant is held for this cog version")]
35    NoGrant,
36    /// A grant is held but is not valid now.
37    #[error("the checkout grant for this cog version has lapsed (expired, withdrawn or its key revoked)")]
38    GrantLapsed,
39    /// A valid grant exists but lists no binary with this (sha256, blake3).
40    #[error("the valid checkout grant does not list this binary")]
41    NotInGrant,
42    /// The binary's BLAKE3 is revoked (this also withdraws its approval).
43    #[error("the binary's hash is revoked")]
44    HashRevoked,
45    /// No operator approval for this cog version covers the binary's sha256.
46    #[error("no operator approval covers this binary")]
47    NoApproval,
48    /// This daemon is another tenant's on a machine of a Seed-licensed mesh:
49    /// Cognitum cogs there run under the machine's licence holder (the
50    /// cluster owner's daemon), never on the ADR-105 path here.
51    #[error("Cognitum cogs on this machine run under its licence holder, not this daemon ({0})")]
52    NotHolder(String),
53}
54
55impl RunRefusal {
56    /// The stable code.
57    pub fn code(&self) -> &'static str {
58        match self {
59            Self::BindingInactive(_) => "binding_inactive",
60            Self::NoGrant => "no_grant",
61            Self::GrantLapsed => "grant_lapsed",
62            Self::NotInGrant => "not_in_grant",
63            Self::HashRevoked => "hash_revoked",
64            Self::NoApproval => "no_approval",
65            Self::NotHolder(_) => "not_holder",
66        }
67    }
68
69    /// What the operator does about it.
70    pub fn remedy(&self, cog_id: &str, version: &str) -> String {
71        match self {
72            Self::BindingInactive(_) => "check `weaver workload node status`".into(),
73            Self::NoGrant | Self::NotInGrant => {
74                format!("weaver cog checkout {cog_id}@{version} --arch <arch>")
75            }
76            Self::GrantLapsed => "renew through the steward (a lapsed licence stops new starts)".into(),
77            Self::HashRevoked => "the artifact was revoked; it cannot run".into(),
78            Self::NoApproval => format!("weaver cog checkout approve {cog_id}@{version}"),
79            Self::NotHolder(_) => {
80                format!("place {cog_id}@{version} from the cluster owner's daemon (the machine's licence holder)")
81            }
82        }
83    }
84}
85
86/// What the gate decided when it did not refuse.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum RunVerdict {
89    /// This node holds no Seed binding: the gate does not apply.
90    NotSeedBound,
91    /// Grant and approval cover the binary.
92    Permit(RunPermit),
93}
94
95/// Decide `req`. `approvals` is `None` when this node has no approval store
96/// (no mesh): every Cognitum run is then refused for `no_approval` once the
97/// grant check passes.
98pub fn check_run(
99    grants: &CheckoutGrantStore,
100    approvals: Option<&ApprovalStore>,
101    req: &RunRequest<'_>,
102) -> Result<RunVerdict, RunRefusal> {
103    let approvals_held = approvals.is_some_and(|a| !a.is_empty() || a.poisoned().is_some());
104    let bound_signal = grants.held_binding().is_some()
105        || grants.poisoned().is_some()
106        || grants.was_ever_bound()
107        || approvals_held;
108    if !bound_signal {
109        return Ok(RunVerdict::NotSeedBound);
110    }
111    if grants.held_binding().is_none() && grants.poisoned().is_none() {
112        // Bound before, but the store holds no binding now: the store file
113        // was deleted or replaced. Fail closed until sync brings it back.
114        return Err(RunRefusal::BindingInactive(
115            "this node was Seed-bound but its licence store holds no binding (store missing?)".into(),
116        ));
117    }
118    grants.binding_status().map_err(|e| RunRefusal::BindingInactive(e.to_string()))?;
119    let Some((grant, art)) = grants.valid_grant_for_artifact(req.cog_id, req.version, req.sha256, req.blake3)
120    else {
121        if grants.held_grant(req.cog_id, req.version).is_none() {
122            return Err(RunRefusal::NoGrant);
123        }
124        let valid = grants
125            .verified_grants()
126            .iter()
127            .any(|v| v.grant().cog_id == req.cog_id && v.grant().version == req.version);
128        return Err(if valid { RunRefusal::NotInGrant } else { RunRefusal::GrantLapsed });
129    };
130    if grants.is_hash_revoked(req.blake3) {
131        return Err(RunRefusal::HashRevoked);
132    }
133    let approval_id = approvals
134        .and_then(|a| a.covering(req.cog_id, req.version, req.sha256))
135        .ok_or(RunRefusal::NoApproval)?;
136    Ok(RunVerdict::Permit(RunPermit { grant_id: grant.grant_id, approval_id, blake3: art.blake3 }))
137}
138
139/// The gate a `workload-host` asks before it installs or starts a
140/// Cognitum-origin cog.
141pub trait CognitumRunGate: Send + Sync {
142    /// Decide `req` (hashes computed from the bytes that will run).
143    fn check(&self, req: &RunRequest<'_>) -> Result<RunVerdict, RunRefusal>;
144    /// True when these bytes are known Cognitum bytes (listed by a held
145    /// grant, or a revoked artifact hash) whatever the package says.
146    fn claims(&self, _sha256: &str, _blake3: &str) -> bool {
147        false
148    }
149    /// True when this BLAKE3 is revoked as an `ArtifactHash`.
150    fn revoked(&self, _blake3: &str) -> bool {
151        false
152    }
153}
154
155/// [`CognitumRunGate`] over this node's stores.
156pub struct StoreRunGate {
157    /// The node's grant store.
158    pub grants: Arc<CheckoutGrantStore>,
159    /// The node's approval store (`None` without a mesh).
160    pub approvals: Option<Arc<ApprovalStore>>,
161}
162
163impl CognitumRunGate for StoreRunGate {
164    fn check(&self, req: &RunRequest<'_>) -> Result<RunVerdict, RunRefusal> {
165        check_run(&self.grants, self.approvals.as_deref(), req)
166    }
167
168    fn claims(&self, sha256: &str, blake3: &str) -> bool {
169        self.grants.claims_artifact(sha256, blake3) || self.grants.is_hash_revoked(blake3)
170    }
171
172    fn revoked(&self, blake3: &str) -> bool {
173        self.grants.is_hash_revoked(blake3)
174    }
175}