crates/kernel/ents-gate/src/config.rs
config.rshistorycomment on this file
| 1 | //! The verification epoch, read from `refs/meta/config` (`gate.epoch`). |
| 2 | |
| 3 | use facet::Facet; |
| 4 | use gix_hash::ObjectId; |
| 5 | use gix_object::Find; |
| 6 | use gix_ref_store::RefStoreRead; |
| 7 | |
| 8 | use crate::error::{Error, Result}; |
| 9 | use crate::object::expect_commit; |
| 10 | |
| 11 | /// The slice of `refs/meta/config`'s typed tree the gate consults: the |
| 12 | /// verification epoch (`gate.epoch`). A later, additive narrowing (e.g. a |
| 13 | /// designated-worker roster for `effect.official`) would read the same |
| 14 | /// tree, alongside this field. |
| 15 | /// |
| 16 | /// `model.sdoc` defines no Config entity yet, so this struct is the |
| 17 | /// first (and currently only) definition of the config tree's shape; it |
| 18 | /// lives here rather than in `ents-model` because these are the only |
| 19 | /// fields any crate reads today. When configuration grows non-gate fields |
| 20 | /// (description, role rules, ...), the entity moves to `ents-model` and |
| 21 | /// that change is a storage migration like any other struct change |
| 22 | /// (`meta-ref.migration`). |
| 23 | /// |
| 24 | /// `epoch` is `None` on a config written before verification was turned |
| 25 | /// on. Once it is `Some`, the gate applies the tip invariant to every |
| 26 | /// `refs/meta/*` update; the value records *when* (seconds since the |
| 27 | /// Unix epoch) enforcement began, for audit tooling — the live gate only |
| 28 | /// tests presence, because every proposed update is by definition after |
| 29 | /// the epoch that admits it. |
| 30 | /// |
| 31 | /// # Examples |
| 32 | /// |
| 33 | /// ``` |
| 34 | /// use ents_gate::Config; |
| 35 | /// |
| 36 | /// let config = Config { epoch: Some(1_700_000_000) }; |
| 37 | /// let (root, store) = facet_git_tree::serialize(&config).expect("serialize"); |
| 38 | /// let back: Config = facet_git_tree::deserialize(&root, &store).expect("deserialize"); |
| 39 | /// assert_eq!(back, config); |
| 40 | /// ``` |
| 41 | // @relation(gate.epoch, scope=file) |
| 42 | #[derive(Debug, Clone, Default, PartialEq, Eq, Facet)] |
| 43 | pub struct Config { |
| 44 | /// When the tip invariant came into force, seconds since the Unix |
| 45 | /// epoch; `None` while verification has never been enabled. |
| 46 | pub epoch: Option<u64>, |
| 47 | } |
| 48 | |
| 49 | /// The config recorded by the tree of the commit at `oid`, or an |
| 50 | /// [`Error::Entity`] when the tree does not parse as [`Config`] — an |
| 51 | /// unreadable config fails closed rather than silently disabling the |
| 52 | /// gate. |
| 53 | fn config_at_commit(objects: &dyn Find, oid: ObjectId) -> Result<Config> { |
| 54 | let commit = expect_commit(objects, oid)?; |
| 55 | facet_git_tree::deserialize(&commit.tree, objects) |
| 56 | .map_err(|source| Error::Entity { oid, source }) |
| 57 | } |
| 58 | |
| 59 | /// The epoch recorded by the config tree of the commit at `oid`, or an |
| 60 | /// [`Error::Entity`] when the tree does not parse as [`Config`] — an |
| 61 | /// unreadable config fails closed rather than silently disabling the |
| 62 | /// gate. |
| 63 | pub(crate) fn epoch_at_commit(objects: &dyn Find, oid: ObjectId) -> Result<Option<u64>> { |
| 64 | Ok(config_at_commit(objects, oid)?.epoch) |
| 65 | } |
| 66 | |
| 67 | /// `refs/meta/config`'s current tree, or [`Config::default`] when the |
| 68 | /// config ref does not exist yet — the same "absent means no narrowing in |
| 69 | /// force" reading [`current_epoch`] already gives absence. |
| 70 | fn current_config(refs: &dyn RefStoreRead, objects: &dyn Find) -> Result<Config> { |
| 71 | #[expect( |
| 72 | clippy::expect_used, |
| 73 | clippy::unwrap_in_result, |
| 74 | reason = "CONFIG_REF is a compile-time constant; the doctest below and \ |
| 75 | ents-model's own tests pin its validity" |
| 76 | )] |
| 77 | let name: gix::refs::FullName = ents_model::namespace::CONFIG_REF |
| 78 | .try_into() |
| 79 | .expect("CONFIG_REF is a valid refname"); |
| 80 | match refs.get(name.as_ref())? { |
| 81 | Some(tip) => config_at_commit(objects, tip), |
| 82 | None => Ok(Config::default()), |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | /// The epoch currently in force, read from `refs/meta/config`'s tip; |
| 87 | /// `None` when the config ref does not exist or records no epoch. |
| 88 | // @relation(gate.epoch, gate.policy-as-state, scope=function) |
| 89 | pub(crate) fn current_epoch(refs: &dyn RefStoreRead, objects: &dyn Find) -> Result<Option<u64>> { |
| 90 | Ok(current_config(refs, objects)?.epoch) |
| 91 | } |