git-ents.gitmain
⌘K
foforge
lib.rs158 lines · 7.9 KB · rusthistorycomment on this file
1//! The gate: the one pure admission judgment over ref-store reads
2//! (`docs/spec/gate.sdoc`).
3//!
4//! This crate owns exactly one verb — [`verify`] — evaluated identically
5//! at the three call sites the design names (`gate.call-sites`): hosted
6//! CAS (mandatory, a failing verdict aborts the transaction), local UI
7//! verdict (advisory, a failing verdict annotates), and push pre-flight
8//! (advisory, a prediction that can only go stale). It is deliberately a
9//! separate crate from `receive` (`arch.gate-receive-split`) so the two
10//! advisory call sites link no effect-matching or enqueue logic, and it
11//! consumes only the *read* half of the ref store
12//! (`arch.refstore-read-cas-split`) plus gitoxide's `Find` seam for
13//! objects, so it is statically incapable of writing.
14//!
15//! # Spec coverage
16//!
17//! From `docs/spec/gate.sdoc`:
18//!
19//! - `gate.tip-signed`, `gate.identity-binding`, `gate.owner-mutation`,
20//! `gate.fast-forward` — [`verify`]. Identity binding recomputes each
21//! refname from the tip's signed content per namespace
22//! (`meta-ref.identity-binding`) — a natural-key tree field, a
23//! hash-identified genesis oid enforced by the all-roots walk, a
24//! composite review/result key, an inbox owner — and strictly decodes a
25//! genesis tree, an unknown entry refusing; owner mutation keys an
26//! advance to the genesis signer (∪ admins) or, for a review, the
27//! member the refname names.
28//! - `gate.atomic-cas` — [`verify`] reads the old tip once and returns
29//! it as [`Admission::cas`], the precondition the writer MUST hand to
30//! `RefStore::transaction`; the CAS itself is the store's.
31//! - `gate.signature-artifact` — signatures are read from the commit's
32//! `gpgsig` header and verified in-process; no push certificate is
33//! consulted, and no API here could accept one.
34//! - `gate.policy-as-state` — members and the epoch are read only from
35//! `refs/meta/*` through `RefStoreRead`, so any clone evaluates the
36//! actual policy offline.
37//! - `gate.epoch` — [`Config`]; the tip invariant applies once an epoch
38//! is recorded, and the epoch-setting commit is itself the first gated
39//! tip of the config ref.
40//! - `gate.call-sites`, `gate.verdict-reason` — [`Verdict`], [`Refusal`],
41//! [`Requirement`]; proven identical across call sites by this crate's
42//! parameterized call-site test.
43//! - `gate.adoption-merge`, `gate.adoption-no-fast-forward`,
44//! `gate.same-actor-divergence` — consequences of judging only the tip
45//! plus DAG descent; pinned by the verdict-table tests.
46//! - `gate.principled-split` — refs outside `refs/meta/*` pass as
47//! [`AdmissionKind::CodeRef`]; the tip invariant never applies to
48//! branch refs.
49//! - `gate.bootstrap` — the empty-member-list window admits only a
50//! self-admitting first enrollment; an all-revoked member set fails
51//! closed and never reopens it.
52//!
53//! Partially here, completed by later phases: `gate.mandatory-hosted`
54//! and `gate.advisory-local` are caller policies (`ents-receive`, the
55//! composition roots) — this crate contributes the shared verdict and,
56//! for the advisory sites, the verdict-time reason rendering including
57//! the inbox alternative ([`Refusal`]). The prohibition on cherry-pick
58//! as an adoption mechanism binds the adoption *tooling*
59//! (`sync.adoption-no-cherry-pick`, enforced by `ents-sync`): a
60//! cherry-pick produces an ordinary commit by the placer, which no pure
61//! function over the result could distinguish, so the gate has nothing
62//! to check — gate.sdoc's Adoption section says the same.
63//!
64//! # Authorization model
65//!
66//! "Signed by a member authorized for that refname" uses exactly the
67//! rules the spec pins today: self-run and inbox namespaces are
68//! owner-only — a member of either provenance writes only its own
69//! `refs/meta/self/<member>/*` and `refs/meta/inbox/<member>/*`
70//! segments, and nobody, admins included, writes another member's
71//! (`meta-ref.inbox`) — `refs/meta/effects/*` is admin-only
72//! (`effect.admin-only`), and self-attested members are refused
73//! canonical refs until promoted (`model.member-provenance`).
74//! Finer-grained, config-stored refname rules are a later, additive
75//! narrowing read from the same [`Config`] the epoch already lives on —
76//! e.g. designating worker keys for one effect's canonical results
77//! namespace (`effect.official`) — unbuilt until a caller needs it;
78//! `Config` itself moves to `ents-model` only once configuration grows
79//! fields no gate rule reads.
80//!
81//! Acceptance-time semantics: a signature is judged against the member
82//! entity *currently in force* — the member ref's tip in the same
83//! snapshot the gate reads (`model.member-revocation`). No
84//! commit-supplied timestamp participates, so a revoked key's new
85//! pushes are refused even with a backdated committer date; refs
86//! accepted before the revocation stay valid because acceptance is
87//! never re-judged. A verdict is therefore a pure function of the
88//! proposed update and current repository state: any clone reproduces
89//! it against the same snapshot, and reconstructing what a *past*
90//! acceptance saw is an audit function over the deployment's op log —
91//! explicitly out of scope for this crate and every other crate here.
92//!
93//! # Examples
94//!
95//! A hosted-shaped round trip: enroll a member (pre-epoch, archival),
96//! turn verification on by setting the epoch (the first gated tip of the
97//! config ref), then verify a signed mutation and use the admission's
98//! CAS precondition.
99//!
100//! ```
101//! use ents_gate::{AdmissionKind, Config, Update, Verdict, verify};
102//! use ents_model::{Provenance, namespace};
103//! use ents_testutil::{Keypair, MemRefStore, ObjectStore, enroll_member, write_meta_entity};
104//! use gix_ref_store::Expected;
105//!
106//! let refs = MemRefStore::default();
107//! let objects = ObjectStore::default();
108//! let key = Keypair::from_seed(1);
109//!
110//! // 1. Enrollment lands pre-epoch: history before the epoch is archival.
111//! enroll_member(&refs, &objects, "jdc", &key, Provenance::AdminRegistered, 100);
112//!
113//! // 2. The epoch-setting commit is the first gated tip of refs/meta/config.
114//! let config_ref: gix::refs::FullName = namespace::CONFIG_REF.try_into().expect("valid");
115//! let epoch_tip = write_meta_entity(
116//! &refs, &objects, config_ref, &Config { epoch: Some(200) }, Some(&key), 200,
117//! );
118//!
119//! // 3. From here on, every meta-ref update is judged by the tip invariant.
120//! // (A stand-in for `ents-forge`'s `Issue`: this crate cannot depend on
121//! // `ents-forge`, which itself depends on this crate.)
122//! # #[derive(facet::Facet)]
123//! # struct Issue { title: String, body: String, state: String }
124//! #
125//! let issue = Issue {
126//! title: "t".into(), body: "b".into(), state: "open".into(),
127//! };
128//! // A hash-identified entity's id is its genesis commit's own oid, so the
129//! // ref is named from the signed commit (`meta-ref.identity-binding`).
130//! let tree = facet_git_tree::serialize_into(&issue, &objects).expect("serializes");
131//! let tip = ents_testutil::write_commit(&objects, &ents_testutil::CommitSpec {
132//! tree, parents: vec![], message: "Open an issue".into(), seconds: 300,
133//! }, Some(&key));
134//! let name: gix::refs::FullName = format!("refs/meta/issues/{tip}").try_into().expect("valid");
135//!
136//! // Judge the tip as a proposal against a store that does not yet have
137//! // the ref, the way hosted CAS or pre-flight would.
138//! let before = refs.fetched_copy();
139//! let verdict = verify(&before, &objects, &Update { name, new: Some(tip) })
140//! .expect("evaluates");
141//! let Verdict::Pass(admission) = verdict else { panic!("authorized update passes") };
142//! assert_eq!(admission.kind, AdmissionKind::TipInvariant);
143//! assert_eq!(admission.cas, Expected::MustNotExist);
144//! # let _ = epoch_tip;
145//! ```
146
147mod config;
148mod error;
149mod object;
150mod policy;
151mod signature;
152mod verdict;
153mod verify;
154
155pub use config::Config;
156pub use error::{Error, Result};
157pub use verdict::{Admission, AdmissionKind, Refusal, Requirement, Verdict};
158pub use verify::{Update, verify};