pub struct ServiceRequest {Show 14 fields
pub service: GitService,
pub repo: Repo,
pub prospective: bool,
pub principal: Principal,
pub access: Level,
pub key_fingerprint: Option<Fingerprint>,
pub git_protocol: Option<String>,
pub delivery: Delivery,
pub credential: Option<String>,
pub plan_notice: Option<String>,
pub pack_ceiling: Option<PackCeiling>,
pub pack_mark: Option<Mark>,
pub lfs_allowance: Option<NamespaceAllowance>,
pub cancel: Cancel,
}Expand description
Everything the wire needs to know about the session it is serving.
Fields§
§service: GitServiceWhich service the client asked for.
repo: RepoThe repository, as the catalog holds it, carrying the hash kind the wire
has to echo in object-format (the git wire protocol) and the default branch.
When Self::prospective is set this is the repository the
first-repository door would create — the same value
RepoCatalog::create would write — and it is not in the catalog yet.
prospective: boolThe first-repository door is open for this push, and nothing has
been written. Only ever true for GitService::ReceivePack or an
LFS upload (GitService::LfsTransfer(Upload), 2026-09-28) to a
repository that does not exist, by a principal who may create it. An
LFS upload’s repository is made when its first object arrives. The
transport advertises repo as an empty repository, reads the client’s
commands, checks the hash, and only then asks
Control::admit_push_create to write the row and makes the store. A
client that hangs up at the advertisement leaves no trace.
principal: PrincipalWho is asking. Carried into the push attestation and the event stream.
access: LevelThe level the principal was found to hold, which the wire may need for finer decisions than “may push at all”.
key_fingerprint: Option<Fingerprint>The key this session actually authenticated with.
None means no key did, the anonymous read-only path, which
is a real state and not a missing value, so it is an Option rather
than an empty string.
The server computed this in auth_publickey and looked the principal up
by it, then used to drop it. Downstream, gunnar-cli’s push attestation
and its RefUpdated / RefDeleted events had to guess it back out of
the identity table, and could only do so when the principal held exactly
one key, so a real deploy account with two keys published an empty
fingerprint, and the audit trail could not answer “which key pushed
this”.
It is a parameter of crate::Dispatch::authorise, not something
attached afterwards like ServiceRequest::with_git_protocol, and the
difference is deliberate: an attach-after-the-fact field can be
forgotten, and a forgotten one is silently None, which is exactly the
shape of the bug this closes. The compiler asks for it instead.
git_protocol is the other way round for the opposite reason: it comes
from the client, and must not be able to reach the grant check.
git_protocol: Option<String>The raw value of the client’s GIT_PROTOCOL environment variable, if it
sent one and the server’s allowlist accepted it (see
crate::server::ENV_ALLOWLIST).
Raw, not parsed, and deliberately: the grammar is
gunnar-wire’s (ProtocolVersion::from_git_protocol), and this crate
does not depend on gunnar-wire. Parsing it here would be a second
implementation of a version negotiation that already exists, and the two
could disagree about version=1 (one implementation).
None means the client sent nothing, which is v0. It is an Option
rather than a defaulted ProtocolVersion so that “the client asked for
v0” and “the client never got to ask” stay distinguishable in a log.
delivery: DeliveryWhich part of the conversation this call carries.
Delivery::Session for SSH, always. Attached afterwards like
ServiceRequest::git_protocol rather than taken as a parameter of
crate::server::Dispatch::authorise, and unlike the key fingerprint
that is safe here for a specific reason: forgetting it fails loudly.
An HTTP POST served as a Session writes a ref advertisement in front
of the negotiation, and git rejects that on the first packet. The
fingerprint’s failure mode is the opposite — a forgotten one is silently
None and shows up months later as an empty field in an audit trail —
which is why that one is a parameter and this one is not.
credential: Option<String>Which credential authenticated this request, as the audit trail
should name it: key:SHA256:… or token:<hash prefix>.
None means a caller that presented no credential at all — the
anonymous read path — and it is the ONLY thing None means here.
It exists because Self::key_fingerprint cannot mean that (GUN-014).
HTTP authenticates with a token, so there is no key to put in that
field and it is None on every HTTP request, git-receive-pack
included — while the code that consumes it states that an empty
fingerprint has “one reason only — the anonymous, read-only path… That
path cannot push”. A push therefore reached RefUpdated, RefDeleted
and the attestation with an empty fingerprint and no other field to say
who had made it.
A builder and not a parameter, unlike the fingerprint, and the reason
is the opposite of that one’s: the SSH front door does not set it at
all. key_fingerprint is already the answer there, and deriving
key:<fp> in one place (transport::session_credential) is one
implementation rather than two front doors spelling the same prefix.
plan_notice: Option<String>The gate chain’s warning line: a Grace tenant’s push is allowed and
told plan expired <date>; renew at <url>. The transport puts it on the
sideband so git push prints remote: …. None for every other state,
for every read, and on a community build (whose one gate never warns).
pack_ceiling: Option<PackCeiling>The gate chain’s byte ceiling for this push, the smallest any gate
asked for. The transport hands it to gunnar-wire, which counts the
pack off the wire and cuts the transfer past it — see
crate::PackCeiling and crate::PlanGate::pack_ceiling.
None for every read, for every write on a namespace with no byte cap,
and on a community build (whose one gate asks for no ceiling), and
None then means what it has always meant: read the pack to its end.
Decided in authorise_service rather than looked up in the transport
for the same reason ServiceRequest::plan_notice is: the gate chain
lives on the front door’s Control, and the transport has never been
able to see it.
§What reads it today, said plainly
The receive path, and only the receive path. The chain asks for a
ceiling wherever it asks pack_quota, which includes an LFS upload — and
the LFS transfer ignores this field, deliberately. What LFS needed was
its own cut against its own meter (lfs_bytes, not pack_bytes), and
since 2026-09-07 it has one: ServiceRequest::lfs_allowance. Half a
ceiling carried here would have been the twin of a limit that fires on
one transport and not the other. A Machine principal is never asked at
all (crate::PlanGate::applies_to_machine), so a primary’s replication
push is never cut by a tenant’s plan.
pack_mark: Option<Mark>Where this push stands in the namespace’s ledger of pack bytes — the
value the pack_bytes counter had when
crate::server::Dispatch::ask_plan measured, taken BEFORE any gate
walked the disk. Some exactly when Self::pack_ceiling is.
gunnar-server binds the two together and hands the result to
gunnar-wire, whose receive loop charges the ledger for every chunk
before it takes it — so the ceiling is the room the whole namespace has
left, not the room each connection is separately told it has.
§Why this is a field beside the ceiling and not a field INSIDE it
The LFS wire folded its mark into the number
(gunnar_lfs::NamespaceAllowance) because there the two are used
together by one callee. Here they are not: the number goes into
gunnar_wire::PackCeiling, in a crate that cannot name a
gunnar_lfs::Mark and must not learn to, and the mark goes into the
same struct as a dyn gunnar_wire::PackLedger. They are joined at
gunnar_server::transport, the seam that already exists for exactly
this conversion, so folding them together here would be folding them for
the journey and nothing else.
The other half of the reason is whose fact it is. crate::PackCeiling
is what a crate::PlanGate ANSWERED, and PlanGate is a public trait
anyone may implement; the mark is the SESSION’s standing in a counter
this crate owns, taken before any gate is consulted and existing whether
or not one answers. Putting it in the gate’s return type would have
asked every implementor to produce something only the dispatch can, and
changed the type of a field the server’s tests read.
lfs_allowance: Option<NamespaceAllowance>The gate chain’s LFS room for this transfer, the smallest any gate
admitted — the twin of ServiceRequest::pack_ceiling one wire over.
The transport hands it to gunnar_lfs::Session (SSH) and to
LfsAccess (smart HTTP), and an upload past it is refused 507 with the
plan’s own sentence, before the object’s bytes.
None for every read, for every namespace with no lfs_bytes cap, and
on a community build — and None means what the pack ceiling’s None
means: take what arrives.
The pack ceiling would not have done: they are different meters on the
signed fact, they are reported separately by Usage.Read, and one is
counted in a receive loop while the other is declared up front. See
crate::PlanGate::lfs_allowance.
§Why a NamespaceAllowance and not the plain gunnar_lfs::Allowance
Because a plain one is a number measured for ONE transfer, and two
transfers that started together measured the same “before”. This one
carries, beside the number, where the transfer stands in the namespace’s
ledger of bytes admitted since — so the door counts what the other
connections are already doing. gunnar-server still decides the number
(PlanGate::lfs_allowance returns the plain Allowance and is
unchanged); crate::server::Dispatch::ask_plan binds it to the
ledger, which is also the one place that can guarantee the mark is taken
BEFORE the disk is walked. See gunnar_lfs::ledger.
cancel: CancelSet when the client is gone and the transfer must stop at its next
safe point (crate::channel::serve_until_session_ends). A transport
whose git work runs off the async task checks it there, so the work
ends before the fan worker serving it is given back.
Implementations§
Source§impl ServiceRequest
impl ServiceRequest
Sourcepub fn with_git_protocol(self, value: Option<String>) -> Self
pub fn with_git_protocol(self, value: Option<String>) -> Self
Attach the client’s GIT_PROTOCOL value.
Separate from the authorisation decision on purpose: what a client may do is decided from its key and the catalog, and an environment variable must never be able to reach that decision. This carries a hint about framing to the wire, nothing more.
Sourcepub fn with_delivery(self, delivery: Delivery) -> Self
pub fn with_delivery(self, delivery: Delivery) -> Self
Say which part of the conversation this call carries. See
Delivery.
Sourcepub fn with_credential(self, credential: Option<String>) -> Self
pub fn with_credential(self, credential: Option<String>) -> Self
Name the credential this request authenticated with. See
ServiceRequest::credential.