Skip to main content

ServiceRequest

Struct ServiceRequest 

Source
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: GitService

Which service the client asked for.

§repo: Repo

The 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: bool

The 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: Principal

Who is asking. Carried into the push attestation and the event stream.

§access: Level

The 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: Delivery

Which 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: Cancel

Set 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

Source

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.

Source

pub fn with_delivery(self, delivery: Delivery) -> Self

Say which part of the conversation this call carries. See Delivery.

Source

pub fn with_credential(self, credential: Option<String>) -> Self

Name the credential this request authenticated with. See ServiceRequest::credential.

Trait Implementations§

Source§

impl Clone for ServiceRequest

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ServiceRequest

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Allocation for T
where T: RefUnwindSafe + Send + Sync,

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more