Skip to main content

clawft_kernel/licence/
store_status.rs

1//! Read-only listings for `weaver cog checkout status` and `weaver doctor`
2//! (ADR-106 phase 3). Nothing here changes state or gates anything.
3
4use serde::Serialize;
5
6use super::approval::Approval;
7use super::store::grant_valid;
8use super::{ApprovalStore, CheckoutGrantStore, GrantArtifact};
9
10/// One (cog, version) slot of the grant store.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub struct GrantRow {
13    /// Cog id.
14    pub cog_id: String,
15    /// Version.
16    pub version: String,
17    /// The held grant's `seq`.
18    pub seq: u64,
19    /// Its id.
20    pub grant_id: String,
21    /// Unix seconds.
22    pub issued_at: u64,
23    /// Unix seconds (`<= issued_at` for a withdrawal).
24    pub expires_at: u64,
25    /// The licence expiry the grant carries.
26    pub licence_expires: u64,
27    /// A withdrawal (a renewal with `expires_at <= issued_at`).
28    pub withdrawn: bool,
29    /// Valid right now (binding in effect, key not revoked, unexpired by
30    /// `max(now, floor)`).
31    pub valid: bool,
32    /// The artifacts it lists.
33    pub artifacts: Vec<GrantArtifact>,
34}
35
36/// One held approval.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38pub struct ApprovalRow {
39    /// The content key (provenance `approval_id`).
40    pub approval_id: String,
41    /// The approval.
42    #[serde(flatten)]
43    pub approval: Approval,
44    /// False when it names another mesh id (orphaned by a `mesh_nonce` change).
45    pub active: bool,
46}
47
48impl CheckoutGrantStore {
49    /// Every held grant, valid or not, by (cog, version). Validity is judged
50    /// as the policy judges it; without a binding in effect none is valid.
51    pub fn grant_rows(&self) -> Vec<GrantRow> {
52        self.run(|inner, ev| {
53            let bound = self.binding_in_effect(inner, ev).ok();
54            let eff = bound.as_ref().map(|b| {
55                let revoked = self.key_revoked(&b.grant_pubkey);
56                (self.eff_now(inner, &b.grant_pubkey), b.mesh_id.clone(), revoked)
57            });
58            inner
59                .slots
60                .values()
61                .filter_map(|s| s.current.as_ref())
62                .map(|h| {
63                    let g = &h.body;
64                    let valid = eff.as_ref().is_some_and(|(now, mesh, revoked)| {
65                        !revoked && &g.mesh_id == mesh && grant_valid(g, *now)
66                    });
67                    GrantRow {
68                        cog_id: g.cog_id.clone(),
69                        version: g.version.clone(),
70                        seq: g.seq,
71                        grant_id: g.grant_id.clone(),
72                        issued_at: g.issued_at,
73                        expires_at: g.expires_at,
74                        licence_expires: g.licence.expires,
75                        withdrawn: g.is_withdrawal(),
76                        valid,
77                        artifacts: g.artifacts.clone(),
78                    }
79                })
80                .collect()
81        })
82    }
83
84    /// `max(now, floor)` for the bound key, the time grants are judged by.
85    pub fn effective_now(&self) -> Option<u64> {
86        self.run(|inner, ev| {
87            let b = self.binding_in_effect(inner, ev).ok()?;
88            Some(self.eff_now(inner, &b.grant_pubkey))
89        })
90    }
91}
92
93/// The sticky "this node was Seed-bound" marker, beside the store file. It
94/// outlives a deleted or replaced store file, so the run gate fails closed
95/// instead of treating such a node as never bound.
96pub const BOUND_MARKER: &str = "licence-bound.marker";
97
98impl CheckoutGrantStore {
99    fn marker(&self) -> std::path::PathBuf {
100        self.path.with_file_name(BOUND_MARKER)
101    }
102
103    /// Write the marker (best effort; logged on failure).
104    pub(super) fn note_bound(&self) {
105        let m = self.marker();
106        if m.exists() {
107            return;
108        }
109        if let Err(e) = super::persist::write_atomic(&m, b"bound\n") {
110            tracing::warn!(error = %e, "could not write the licence bound marker");
111        }
112    }
113
114    /// True once this node has accepted a binding (the marker exists, or a
115    /// binding is held now, which also writes the marker).
116    pub fn was_ever_bound(&self) -> bool {
117        if self.marker().exists() {
118            return true;
119        }
120        if self.held_binding().is_some() {
121            self.note_bound();
122            return true;
123        }
124        false
125    }
126
127    /// True when a held grant (current or previous) lists a binary with
128    /// this sha256 or this BLAKE3: the bytes are a checked-out Cognitum cog,
129    /// whatever package they arrive in.
130    pub fn claims_artifact(&self, sha256: &str, blake3: &str) -> bool {
131        let g = self.lock();
132        g.slots
133            .values()
134            .flat_map(|s| s.current.iter().chain(s.previous.iter()))
135            .flat_map(|h| h.body.artifacts.iter())
136            .any(|a| a.sha256 == sha256 || a.blake3 == blake3)
137    }
138}
139
140impl ApprovalStore {
141    /// Every held approval, active or orphaned.
142    pub fn rows(&self) -> Vec<ApprovalRow> {
143        let local = self.local.get().map(|m| m.to_hex());
144        let g = self.lock();
145        g.0.iter()
146            .map(|(k, (_, a))| ApprovalRow {
147                approval_id: k.clone(),
148                approval: a.clone(),
149                active: g.1.is_none() && Some(&a.mesh_id) == local.as_ref(),
150            })
151            .collect()
152    }
153
154    /// The signed envelopes of the orphaned approvals: the CLI verifies each
155    /// against the operator's own key before it re-signs it.
156    pub fn orphaned_signed(&self) -> Vec<super::SignedApproval> {
157        let local = self.local.get().map(|m| m.to_hex());
158        let g = self.lock();
159        g.0.values().filter(|(_, a)| Some(&a.mesh_id) != local.as_ref()).map(|(s, _)| s.clone()).collect()
160    }
161
162    /// The orphaned approvals themselves (`--reapprove-orphaned` re-signs
163    /// them for the new mesh id).
164    pub fn orphaned_approvals(&self) -> Vec<Approval> {
165        self.rows().into_iter().filter(|r| !r.active).map(|r| r.approval).collect()
166    }
167}