1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! **The plan gate: the front door's one question before the bytes.**
//!
//! `authorise_service` (see [`crate::server::Dispatch`]) is the ONE choke point
//! SSH and smart-HTTP share, and it is where a push is refused **before any
//! pack bytes**, which is the rule that makes this a front-door check and not a
//! `gunnar-policy` gate (a gate runs after the pack is read; that is a 1 GB
//! upload accepted and then refused). This crate does not know what a plan is,
//! what monetize is, or how a fact is signed; it asks the [`PlanGate`]s the
//! server hands in and does what the verdict says.
//!
//! # Two implementations, one chain (decided 2026-09-04)
//!
//! > two implementations: 1) the free lunch, basically checking disk has size
//! > before a new repo is created; 2) the monetize-bound one
//!
//! 1. [`crate::headroom::DiskHeadroomGate`] — **always compiled, no monetize
//! dependency.** Refuses a push and a `Repos.Create` when the store root's
//! filesystem would drop under a floor. The community binary has this one
//! link.
//! 2. `gunnar-server::entitlement::EntitlementSlot` (`metered` only) — the
//! ladder: Suspended, Retention, the pack quota. The decision is
//! `monetize_embed::EntitlementCache`'s, one atomic load and a map lookup.
//!
//! [`crate::Control`] holds them as a **chain**, asked in order, disk first;
//! the first `Refuse` wins and is the line the client is told. A box that is
//! full is told it is full, not that its plan lapsed — the disk is the fact
//! nobody can pay their way past. A gate that always answers `Allow` is
//! invisible to a request that only pushes valid things (measured, not asserted), which is why
//! every gate's refusal has a test that sets the condition and watches the
//! push die before a byte.
//!
//! # What is asked, and when
//!
//! | request | asked | may be refused |
//! |---|---|---|
//! | `git-receive-pack`, LFS upload | [`PlanAction::Write`], then [`PlanGate::pack_quota`] and [`PlanGate::pack_ceiling`] | Suspended, Retention, over `caps["pack_bytes"]` — before the bytes, and (receive-pack only) DURING them |
//! | anonymous `git-upload-pack` / LFS download | [`PlanAction::AnonymousRead`] | Suspended, Retention |
//! | the owner's, or any authenticated reader's, `git-upload-pack` | nothing | **never** — data is never held hostage |
//!
//! Asked only for a request the grant check already admitted, so a refusal
//! here never tells a stranger anything about a repository they could not
//! already see; and asked AFTER the standby freeze, so a standby stays a
//! standby whatever the plan says.
//!
//! # The one answer that is not a verdict
//!
//! [`PlanGate::pack_ceiling`] is the exception to "the front door decides
//! before the bytes", and it is the exception because it has to be: a pack
//! header carries an **object count, not a byte count**, so the size of a push
//! is not knowable here. The gate therefore answers with a NUMBER rather than a
//! verdict, the number rides on [`crate::ServiceRequest::pack_ceiling`], and
//! the refusal happens in `gunnar-wire`'s receive loop the moment the count
//! passes it. That is still the front door's decision — it is just carried to
//! where the bytes are.
/// What the request is about to do to the namespace's data.
/// The gate's answer.
/// **A byte ceiling for one push, and the line the client is told past it.**
///
/// The twin of `gunnar_wire::receive_pack::PackCeiling`, which is where it is
/// enforced. Two declarations of two fields rather than one shared type,
/// deliberately: this crate does not depend on `gunnar-wire` and must not start
/// — the same boundary [`crate::ServiceRequest::git_protocol`] is a raw
/// `String` for. `gunnar-server` holds both and converts, which is
/// four lines at one seam (`transport.rs`) rather than a dependency edge
/// between two crates that have stayed apart on purpose. There is no logic here to duplicate: the deciding
/// is in the gate below, the enforcing is in the receive loop, and this is the
/// number travelling between them.
/// One link of the front door's chain: "may this namespace do this, right now".
///
/// **Nanoseconds, no I/O, no lock held across I/O.** `verdict` is on the path
/// of every push and every anonymous clone; an implementation that reached a
/// network or a disk here would put a payment provider's uptime in front of
/// `git push`, which is the outage the metered plan exists to forbid.
/// `pack_quota` may read the disk (it is the namespace's byte count, or the
/// filesystem's free space), and is asked only for a write that grows data.