Help us shape the future of Go! Take the 2026 Go Developer Survey

tenancy

package
v3.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package tenancy is the register of every decision this server makes about who a caller is and what it may hold, spend or be told.

It decides and never enforces. The layers that count streams, fill token buckets, evict pool entries and write refusals keep doing so where the resource is counted; what lives here is the answer they apply, written once:

  • the key vocabulary (Key), where each key says what it costs a caller to mint another value of it, which is the fact issues 540 and 561 each had to rediscover by hand;
  • one Decision row for each requirement of the tenant policy specification, with its key, class, kind, disposition, value source, zero meaning, capacity behavior and refusals (Decisions);
  • the policy values and refusal codes the layers read (values.go, codes.go);
  • the rules promoted here from the layers, so that changing one is an edit of the register: which bucket each MCP method is charged to (MeterFor, meter.go), which holdings make a pool entry busy (Busy, busy.go), and which settings switch an authentication budget off and when the transport-source budget exists (BudgetOn, EscalationOn, TransportSourceBudgetOn, budget.go);
  • the table of authentication failures and what each is charged (Failures);
  • the channels go-sdk v1.8.0 carries for each method (Carriages);
  • Validate and ValidateFailures, which turn the specification's invariants into errors, so that a share on a key a caller can mint, a per-caller ceiling on a process resource with no process partner, or a refusal on a channel the method cannot carry is refused before review.

The specification it implements is docs/development/tenant-policy-spec.md, and the decision to hold policy this way is ADR-0023 (docs/development/adr/adr-0023-tenant-policy-is-declared-once.md). The comments here cite that specification as "spec", by the heading of the section they rest on ("(spec: Keys)", "(spec: Invariants)"), and by the IDs it shares with this package: a register row ("HLD-001"), an invariant ("INV-003"), a validation item ("VAL-007") or a finding ("F-04").

A leaf with nothing to initialize

The package imports only errors, fmt, strings and time, all of which the server binary already links, and it declares no package-level variable and no init function: every table is built by the function that returns it, on each call. So importing it adds no initialization work to a binary, a function nothing reachable calls is dropped by the linker, and a layer that replaces a literal with one of the constants here compiles to the same instructions and data. The tests in doc_test.go hold all three properties, because the proof that moving a value here changed nothing rests on them.

A promoted rule is the one thing here a layer calls, so the proof that promoting it changed nothing is not the binary. It is an oracle test and a fuzz target holding the function to a verbatim copy of the code it replaced, and the function allocates nothing and leaves the site's lock profile as it was: Busy reads the watcher count through Holdings, which takes the subscription manager's lock, and it does so exactly when the code it replaced did, only when no stream is open.

Findings

A row that disagrees with the specification's invariants today carries the finding that records the disagreement, and Validate accepts it only because it does. No finding is fixed here: each is filed as an issue of its own, and FindingIssue names it. When that issue answers the finding, the rows stop carrying it and the finding stays in AllFindings with its issue. An answer is a change of code, as issue 951's RTC-007 answered F-03, or a decision that the departure is the position, recorded in the Decided of the rows it concerns, as issue 959 answered F-19 on RTC-001 and F-33 on IDN-013.

Index

Constants

View Source
const (
	// MethodGate is the HTTP gate, which answers before the SDK sees the
	// request.
	MethodGate = "http"
	// MethodStartup is the process refusing to start.
	MethodStartup = "startup"
	// MethodEviction is the server ending what a pool entry held when the
	// entry is evicted.
	MethodEviction = "eviction"
	// MethodExpiry is the SDK ending a stateful session that sat idle.
	MethodExpiry = "expiry"
)

The pseudo-methods a Refusal names where no MCP method carries it.

View Source
const (
	// CodeUnauthorized mirrors HTTP 401.
	CodeUnauthorized = -40100
	// CodeForbidden mirrors HTTP 403.
	CodeForbidden = -40300
	// CodeTooManyRequests mirrors HTTP 429, at the gate and in-band.
	CodeTooManyRequests = -42900
	// CodeUnavailable mirrors HTTP 503.
	CodeUnavailable = -50300
	// CodeServerBusyLegacy is the in-band code the listen and watcher
	// ceilings, a GitLab 429 on a subscription's first read and shutdown
	// refuse with.
	CodeServerBusyLegacy = -32000
)

The policy refusal codes, which the layers alias.

A code this server allocates for a purpose the MCP specification does not define belongs outside the JSON-RPC reserved range (-32768 to -32000), so the gate's codes mirror their HTTP status multiplied by -100 and the in-band "retry later" code mirrors 429 the same way. CodeServerBusyLegacy is the exception the specification records: it sits in the legacy sub-range that new implementations should not use, and issue 956 is where that is decided.

View Source
const (
	// SessionHeldDivisor is the share of the held-request ceiling the
	// stateful sessions may number, as its reciprocal: a half, so the held
	// slot each session takes for its standalone stream leaves at least half
	// of the held slots to calls (HLD-010).
	SessionHeldDivisor = 2
	// HeldRequestDescriptors is how many file descriptors one request the
	// process holds open costs it: the caller's connection and the one to
	// GitLab (HLD-011, measured through cmd/bench_resources' held mode).
	HeldRequestDescriptors = 2
	// DescriptorSpareDivisor is the share of the process's descriptor limit
	// the held-request ceiling leaves spare, as its reciprocal: an eighth
	// (HLD-011).
	DescriptorSpareDivisor = 8
	// FallbackDescriptorLimit is the descriptor limit the held-request
	// ceiling is sized against where the platform has none to read (HLD-011).
	FallbackDescriptorLimit = 1024
	// ListenStreamsPerCredential is how many subscriptions/listen streams one
	// credential may hold open (HLD-001).
	ListenStreamsPerCredential = 64
	// ListenStreamsPerProcess is how many subscriptions/listen streams the
	// process holds open across every credential (HLD-002).
	ListenStreamsPerProcess = 512
	// WatchersPerCredential is how many resource watchers one credential's
	// manager holds (HLD-003).
	WatchersPerCredential = 10
	// WatchersPerProcess is how many resource watchers the process holds
	// across every credential (HLD-004).
	WatchersPerProcess = 512
	// WatchLease is how long a watch runs at its own cadence without traffic
	// before it is demoted (HLD-007).
	WatchLease = 30 * time.Minute
	// WatchSlowInterval is the poll a demoted watch runs at (HLD-007).
	WatchSlowInterval = 10 * time.Minute
	// WatchMaxLifetime is when a watch stops however it is used (HLD-007).
	WatchMaxLifetime = 24 * time.Hour
	// WatchBaseInterval is the poll a watch starts at (HLD-007).
	WatchBaseInterval = 15 * time.Second
	// WatchMinInterval is the fastest poll a watch may run at (HLD-007; the
	// five-second floor HLD-003's own reason is sized against).
	WatchMinInterval = 5 * time.Second
	// WatchRateLimitPause is the first pause of every watcher of a manager
	// that saw a GitLab 429 (RTC-005).
	WatchRateLimitPause = 30 * time.Second
	// WatchRateLimitPauseMax is the longest that pause doubles to (RTC-005).
	WatchRateLimitPauseMax = 5 * time.Minute
	// WatchRateLimitJitter is the fraction the pause is spread by (RTC-005).
	WatchRateLimitJitter = 0.2
)

Holdings (HLD-001 to HLD-004, HLD-007, HLD-010, HLD-011, RTC-005).

View Source
const (
	// ToolCallRateHTTP is the tool-call bucket's refill, in requests a second,
	// an HTTP deployment gets unless it says otherwise (RTC-001).
	ToolCallRateHTTP = 10
	// ToolCallRateEnvDefault is the refill stdio gets when nothing is set:
	// zero, which switches the limiter off (RTC-001; the spec changes no
	// value). The HTTP environment overlay passes it too, but only as its
	// parser's default for an empty value, which the overlay's presence check
	// never lets through, so HTTP with nothing set keeps ToolCallRateHTTP.
	ToolCallRateEnvDefault = 0
	// ToolCallBurst is the tool-call bucket's size (RTC-001).
	ToolCallBurst = 40
	// ToolCallRateMax is the highest refill an operator may configure
	// (RTC-001).
	ToolCallRateMax = 1000
	// ToolCallBurstMax is the largest bucket an operator may configure
	// (RTC-001).
	ToolCallBurstMax = 10000
	// CompletionFactor is how much larger and faster the completion bucket is
	// than the tool-call one (RTC-002).
	CompletionFactor = 10
	// CatalogDivisor is how much slower the tools/list bucket refills than the
	// tool-call one (RTC-003).
	CatalogDivisor = 10
	// CatalogProcessRate is how many tools the process lists a second across
	// every credential, the refill of the tools/list bucket keyed on the
	// process beside each entry's (RTC-007).
	CatalogProcessRate = 3000
	// CatalogProcessBurst is how many tools that bucket holds (RTC-007).
	CatalogProcessBurst = 48000
	// UpstreamRetries is how many times a failed GitLab request is re-sent
	// (RTC-006).
	UpstreamRetries = 2
	// UpstreamRetryWaitMax caps one wait between attempts (RTC-006).
	UpstreamRetryWaitMax = 5 * time.Second
	// UpstreamRetryStep is the per-attempt wait for a failure that is not a
	// 429 (RTC-006).
	UpstreamRetryStep = 700 * time.Millisecond
)

Rates (RTC-001 to RTC-003, RTC-006, RTC-007).

View Source
const (
	// PoolSize is how many entries the pool keeps unless the operator says
	// otherwise (POL-001).
	PoolSize = 100
	// PoolSizeMax is the largest pool an operator may configure (POL-001).
	PoolSizeMax = 10000
	// PoolIdleTimeout is how long an entry may go unused before it is
	// reclaimed (POL-004).
	PoolIdleTimeout = 1 * time.Hour
	// PoolIdleTimeoutMax is the longest idle timeout an operator may configure
	// (POL-004).
	PoolIdleTimeoutMax = 24 * time.Hour
	// PoolIdleSweepDivisor sets the idle sweep's cadence as a fraction of the
	// timeout (POL-004).
	PoolIdleSweepDivisor = 4
	// PoolIdleSweepFloor is the floor under that cadence (POL-004).
	PoolIdleSweepFloor = 1 * time.Minute
	// CredentialProbes is how many credential probes the pool runs at once
	// (POL-006).
	CredentialProbes = 16
	// CredentialProbeWait is how long a build waits for a probe slot
	// (POL-006).
	CredentialProbeWait = 5 * time.Second
)

The pool (POL-001, POL-004, POL-006).

View Source
const (
	// UpstreamRetryAfter is the Retry-After a gate refusal advertises when
	// GitLab failed the verification without saying when to come back
	// (ADM-002), and the one the three ceilings on the process's own work
	// advertise, the verification slots (ADM-014), the stateful sessions
	// (HLD-010) and the held requests (HLD-011).
	UpstreamRetryAfter = 30 * time.Second
	// OAuthCacheTTL is how long a verified OAuth token is reused (ADM-005).
	OAuthCacheTTL = 15 * time.Minute
	// OAuthCacheTTLFloor is the shortest cache lifetime an operator may
	// configure (ADM-005).
	OAuthCacheTTLFloor = 1 * time.Minute
	// OAuthCacheTTLMax is the longest cache lifetime an operator may configure
	// (ADM-005).
	OAuthCacheTTLMax = 2 * time.Hour
	// OAuthCacheSweepDivisor sets the cache sweep's cadence as a fraction of
	// the lifetime (ADM-005).
	OAuthCacheSweepDivisor = 4
	// OAuthCacheSweepFloor is the floor under that cadence (ADM-005).
	OAuthCacheSweepFloor = 30 * time.Second
	// OAuthCacheCapacity is how many verified OAuth identities the cache
	// holds before an expired or least recently used one makes room
	// (ADM-005). It equals PoolSizeMax, the largest pool an operator may
	// configure, and the values test holds the two equal.
	OAuthCacheCapacity = 10000
	// OAuthVerifications is how many OAuth token verifications the process
	// runs at once (ADM-014).
	OAuthVerifications = 16
	// OAuthVerificationWait is how long a verification waits for one of those
	// slots before it is refused (ADM-014).
	OAuthVerificationWait = 5 * time.Second
	// RejectedTokenTTL is how long a rejected token is remembered (ADM-006).
	RejectedTokenTTL = 5 * time.Minute
	// RejectedTokenCapacity is how many rejected tokens are remembered
	// (ADM-006).
	RejectedTokenCapacity = 4096
	// CredentialMaxAge is the longest an entry serves on the strength of one
	// credential check (ADM-008).
	CredentialMaxAge = 1 * time.Hour
	// CredentialMaxAgeCeiling is the largest age the pool option honors
	// (ADM-008).
	CredentialMaxAgeCeiling = 24 * time.Hour
	// RevalidateInterval is how often every entry's credential is re-probed
	// (ADM-009).
	RevalidateInterval = 15 * time.Minute
	// RevalidateIntervalMax is the longest interval an operator may configure
	// (ADM-009).
	RevalidateIntervalMax = 24 * time.Hour
	// UnexplainedRefusalCooldown is how long a 401 that named no cause waits
	// before another one is confirmed with a probe (ADM-010).
	UnexplainedRefusalCooldown = 30 * time.Second
	// RequestStateTTL is how long signed multi-round-trip request state stays
	// usable (IDN-011).
	RequestStateTTL = 10 * time.Minute
	// SessionIdleTimeout is how long a stateful session may sit idle before
	// the SDK closes it (END-005).
	SessionIdleTimeout = 30 * time.Minute
	// SessionIdleTimeoutMax is the longest idle timeout an operator may
	// configure (END-005).
	SessionIdleTimeoutMax = 24 * time.Hour
)

Admission (ADM-002, ADM-005, ADM-006, ADM-008 to ADM-010, ADM-014, IDN-011, END-005).

View Source
const (
	// AuthFailureLimit is how many failed authentications one address may
	// produce inside the window before it is blocked (AUB-001).
	AuthFailureLimit = 10
	// AuthFailureWindow is the window that budget counts in, and the step the
	// distinct-credential escalation is built from (AUB-001, AUB-003).
	AuthFailureWindow = 1 * time.Minute
	// AuthFailureLimitMax is the largest limit an operator may configure
	// (AUB-001).
	AuthFailureLimitMax = 100000
	// AuthFailureWindowMax is the longest window an operator may configure
	// (AUB-001).
	AuthFailureWindowMax = 24 * time.Hour
	// TransportSourceDistinctKeys is how many distinct failing primary keys
	// one transport source may produce inside the window (AUB-002).
	TransportSourceDistinctKeys = 500
	// AuthDistinctTokenLimit is how many distinct refused credentials one
	// address may have inside its window before it is blocked (AUB-003).
	AuthDistinctTokenLimit = 50
	// AuthDistinctTokenWindow is the window that budget counts in (AUB-003).
	AuthDistinctTokenWindow = 10 * time.Minute
	// AuthDistinctTokenLimitMax is the largest limit an operator may configure
	// (AUB-003).
	AuthDistinctTokenLimitMax = 100000
	// AuthDistinctTokenWindowMax is the longest window an operator may
	// configure (AUB-003).
	AuthDistinctTokenWindowMax = 24 * time.Hour
	// AuthEscalationFirst is the first block's length in steps (AUB-003).
	AuthEscalationFirst = 1
	// AuthEscalationSecond is the second block's length in steps (AUB-003).
	AuthEscalationSecond = 10
	// AuthEscalationThird is the third and every later block's length in
	// steps (AUB-003).
	AuthEscalationThird = 60
	// AuthTrackedSources is how many addresses the failure table tracks
	// (AUB-004).
	AuthTrackedSources = 4096
	// AuthSweepInterval is how often the authentication tables are swept
	// (AUB-005).
	AuthSweepInterval = 5 * time.Minute
)

Authentication failure budgets (AUB-001 to AUB-005).

View Source
const (
	// TierNamespacePageSize is the page size of the namespace listing the
	// tier probe reads (AUT-003).
	TierNamespacePageSize = 100
	// TierNamespaceMaxPages is how many pages of it the probe reads at most
	// (AUT-003).
	TierNamespaceMaxPages = 10
)

The tier (AUT-003).

Variables

This section is empty.

Functions

func BudgetOn

func BudgetOn(limit int, window time.Duration) bool

BudgetOn reports whether the per-address failure budget is on: its limit and its window are both above zero (register row AUB-001).

Zero means off (INV-015), and so does a negative figure. The server then builds no limiter at all, because the limiter blocks once a count reaches its limit, so one built with a limit of zero would block an address on its first failure: the harshest setting there is, reached by typing the figure every other budget reads as none. The limiter, its table and its sweep stay where the failures are counted; what lives here is which settings mean off, so that changing that is an edit to the register and to the oracle test beside it rather than to a condition in a layer.

func Busy

func Busy(h Holdings) bool

Busy reports whether an entry is doing work the pool cannot see: holding an open listen stream, or at least one watcher (POL-003).

A watcher polls GitLab on its own, and an open subscriptions/listen is a request the client holds rather than repeats, so neither refreshes the pool entry that owns them. The pool asks this before it evicts an idle entry, and prefers an entry that is not busy under size pressure.

The stream count is read first, and the watcher count only when no stream is open. The pool asks under its write lock, where the one lock this may take is the manager's, so reading the watchers only when the answer still depends on them is what keeps that lock taken exactly as often as it was when the server decided this itself. It allocates nothing. It is not inlined, since two calls through the interface cost more than the compiler's inlining budget; the predicate it replaced was not inlined either, so what the pool pays beyond it is one or two dynamic dispatches per entry it asks about, where that predicate read its counters directly.

func Carries

func Carries(method string, era Era, ch Channel) bool

Carries reports whether a refusal on channel ch can reach the caller of method in era.

A refusal for EraAny is carried when some era the method exists in carries it. On stdio there is no HTTP status and no session to close, so the gate and a session close are never carried there, whatever the method.

func EscalationOn

func EscalationOn(limit int, window, step time.Duration) bool

EscalationOn reports whether the distinct-credential budget is on: its limit, its window and its escalation step are all above zero (register row AUB-003).

The step is not a setting of its own. The server passes AUB-001's window, so that the ladder is one fast window, then ten, then sixty, and a window of zero, which switches AUB-001 off, switches this budget off with it. That coupling departs from INV-015's clause that switching one limit off does not switch off another, and is recorded as a finding (issue 958); it is stated here rather than changed, since this function decides exactly what the condition it replaced decided.

func FindingIssue

func FindingIssue(id string) int

FindingIssue returns the number of the issue the finding id is filed as, or zero for an id that names no finding.

func TransportSourceBudgetOn

func TransportSourceBudgetOn(trustedProxyHeader string) bool

TransportSourceBudgetOn reports whether the transport-source budget exists: only when the deployment names a trusted-proxy header, one that is not blank once the whitespace around it is trimmed (register row AUB-002).

A trusted header is the only configuration in which the per-address budget's key is chosen by the caller, so it is the only one in which this budget has anything to catch: it counts, per address a connection actually came from, the distinct keys that failed through it. Without the header the per-address budget is already keyed on that address, and a second limiter over the same string would only halve it. The budget's window falls back to the default when AUB-001's is zero, in three places the register declares rather than moves (issue 958).

func Validate

func Validate(ds []Decision) error

Validate checks the rows against the invariants (spec: Invariants) and returns every violation joined, or nil.

It is what answers, before review, the questions issues 540 and 561 each answered by hand: a share on a key a caller can mint is refused with the key's evidence in the message, a per-caller ceiling on a process resource with no process partner is refused, and so is a refusal on a channel the method cannot carry. A rule that can be excused passes a row breaking its invariant only through a finding recorded for it (see Finding.Invariants); some rules cannot be excused at all, and two are excused by a recorded decision instead: checkAcrossKeys, and the half of checkZeroStated that lets a process partner follow the row it stands beside. The specification's section on invariants says which is which.

func ValidateFailures

func ValidateFailures(fs []Failure) error

ValidateFailures checks the authentication failure table and returns every violation joined, or nil.

A failure is charged only when the caller caused it (INV-007): a missing credential, or one GitLab refused. An upstream outage, an unanswered introspection, probe saturation, an under-scoped but genuine token, an unadmitted application or a misaddressed instance says nothing about the credential, and charging it would let a GitLab outage lock out callers holding valid tokens.

Types

type Answer

type Answer uint8

Answer is the one class of next action a refusal tells its caller to take (INV-012; spec: Where a refused caller learns what to do).

const (
	// RetryLater is come back later.
	RetryLater Answer = iota + 1
	// Reauthorize is the credential is bad.
	Reauthorize
	// WidenScope is the credential's scope is too narrow.
	WidenScope
	// AskOperator is the deployment's configuration decides.
	AskOperator
	// FixRequest is the request itself is wrong.
	FixRequest
	// StartOver is begin a new session or listen.
	StartOver
	// NoAnswer is none given.
	NoAnswer
)

The answer classes.

func (Answer) String

func (a Answer) String() string

String names the answer class in lower case (spec: Where a refused caller learns what to do).

type Axis

type Axis uint8

Axis says which side of admission a key lives on (spec: Two axes).

const (
	// AxisNone is the axis of [KeyNone].
	AxisNone Axis = iota
	// AxisRequest is one request: bounding it bounds what a request may cost,
	// never what a caller may hold.
	AxisRequest
	// AxisPreAdmission is before a tenant exists, where the address, the source
	// or a refused credential's digest is the only key available.
	AxisPreAdmission
	// AxisRequester is a tenant, a credential or something one credential
	// holds.
	AxisRequester
	// AxisProcess is the process or the deployment, which no caller can
	// multiply.
	AxisProcess
)

The axes a key can lie on.

type Capacity

type Capacity uint8

Capacity is what a structure or a ceiling does when it is full (VAL-006).

const (
	// CapacityNone states none: a structure with no capacity at all.
	CapacityNone Capacity = iota
	// RefuseNewcomer refuses what arrives.
	RefuseNewcomer
	// ReclaimOwnThenRefuse takes back something the same key holds, and
	// refuses when there is nothing to take.
	ReclaimOwnThenRefuse
	// EvictAcrossKeys takes a holding from another key to admit this one.
	// [Validate] requires a recorded decision for it (INV-005).
	EvictAcrossKeys
	// StopCounting stops tracking new keys rather than evicting a record.
	StopCounting
	// EvictOldest evicts the record nearest its expiry.
	EvictOldest
	// WaitThenRefuse queues for a bounded time, then refuses.
	WaitThenRefuse
)

The capacity behaviors.

type Carriage

type Carriage struct {
	// Methods are the MCP methods, or the pseudo-methods [MethodGate],
	// [MethodStartup], [MethodEviction] and [MethodExpiry].
	Methods []string
	// Era is the protocol revision the row holds in; [EraAny] holds in both.
	Era Era
	// Channels are the channels carried.
	Channels []Channel
}

Carriage is one row of the carried-channel matrix: the channels a refusal can reach a caller through, for a set of methods in one era.

The matrix is the refusal channel table as data (spec: Refusal channels), and it is what the go-sdk this module pins does rather than what the protocol would permit: a tools/call can be refused as a result with isError because only that result has the flag, a listing only as a JSON-RPC error, and a status other than the SDK's own 400 and 404 only by the gate in front of it.

It is checked against the SDK rather than asserted by it: channels_integration_test.go drives go-sdk in process through every row whose method the SDK owns, in both eras, over the in-memory transport and over the streamable HTTP handler answering with an event stream and with a JSON body, and fails, naming the row, when a listed channel is not carried in an era the row holds in, or a channel it attempts is carried and the matrix does not list it for that method and era. The rows for MethodGate and MethodStartup describe this server's own layers and are not driven, and neither is MethodEviction, whose row is driven through MethodExpiry. An SDK upgrade that changes what is carried therefore fails its own pull request, which edits this table in the same change.

func Carriages

func Carriages() []Carriage

Carriages returns the carried-channel matrix.

Three channels record narrowing rather than refusal, on the tools/list and tools/call rows: a listing may leave out what the authorization on the request does not reach (Absent), and a call for such an action is answered with its cause (Withheld) or, for the tier, as unknown (Unknown). The listing may vary with the authorization and never with the connection (INV-009), which is why no listing row carries anything a rate or a ceiling could produce except an error.

type Channel

type Channel uint8

Channel is how a refusal or an ending reaches the caller (spec: Refusal channels).

const (
	// Gate is an HTTP status from the gate, before the SDK, with a JSON-RPC
	// body carrying the request id.
	Gate Channel = iota + 1
	// RPC is an in-band JSON-RPC error.
	RPC
	// ToolError is a tools/call result with isError set.
	ToolError
	// EmptyCompletion is a completion with no values.
	EmptyCompletion
	// Withheld is a narrowed surface whose answer names the cause.
	Withheld
	// Absent is a narrowed surface where the action or tool is missing.
	Absent
	// Unknown is a narrowed surface whose answer calls the action unknown.
	Unknown
	// ListenEnd is a subscriptions/listen ended with a completion result
	// carrying a watch-end reason.
	ListenEnd
	// SessionClose is a stateful session closed; later requests get 404.
	SessionClose
	// Silent is no visible refusal: uncounted, delayed or rebuilt.
	Silent
	// Startup is the process refusing to start.
	Startup
)

The channels.

func (Channel) String

func (c Channel) String() string

String names the channel in kebab case (spec: Refusal channels).

type Class

type Class uint8

Class says whether a decision's key agrees with the tenant definition, given the reason the code states for it (spec: Alignment classes). It is the class of the HTTP key: on stdio every per-caller key is the process (spec: Per mode).

const (
	// ClassT is keyed on the tenant itself.
	ClassT Class = iota + 1
	// ClassE is a property of the tenant or instance, resolved with the
	// entry's own credential.
	ClassE
	// ClassC is a property of the credential: validity, authority, admission.
	ClassC
	// ClassR bounds or ends something the entry itself holds.
	ClassR
	// ClassQ is scoped to a request, a session or a URI.
	ClassQ
	// ClassA is before admission, where no tenant exists yet.
	ClassA
	// ClassP is the process or the deployment.
	ClassP
	// ClassU is an allowance keyed on the entry whose stated reason names no
	// unit.
	ClassU
	// ClassD is an allowance keyed on the entry whose reason is per user or
	// per process, so one tenant with N credentials holds N units of it. A
	// class D row always carries a finding.
	ClassD
)

The alignment classes (spec: Alignment classes).

type Decision

type Decision struct {
	// ID is the requirement id, "HLD-001".
	ID string
	// Question is the question it answers (spec: The five questions).
	Question Question
	// Kind is its shape.
	Kind Kind
	// Class is the alignment class of its HTTP key.
	Class Class
	// Disposition is what the register does with it.
	Disposition Disposition
	// Resource is what is held, spent or counted, in a few words.
	Resource string
	// Key is the HTTP key.
	Key Key
	// StdioKey is the key on stdio, KeyNone when the decision does not exist
	// there (VAL-011).
	StdioKey Key
	// StatedUnit is the unit the quoted reason names in its own words, KeyNone
	// when it names none.
	StatedUnit Key
	// ReasonUnit is the unit of what the reason protects or cites, as the
	// specification established it. [Decision.Disagrees] compares it with the
	// key.
	ReasonUnit Key
	// Reason is a short quotation of the code's own stated reason, verbatim.
	Reason string
	// ReasonAt is where that quotation lives.
	ReasonAt Site
	// ProtectsProcess says the reason is a process-wide resource, which is
	// what makes INV-004 ask for a process partner.
	ProtectsProcess bool
	// Table says it governs a structure holding one record per value of Key
	// (INV-010).
	Table bool
	// Partner is the ID of the process ceiling beside it (INV-004).
	Partner string
	// Values are the names of the register constants that are its values.
	Values []string
	// Functions are the names of the register functions that answer part of
	// it.
	Functions []string
	// Source is where its value comes from.
	Source Source
	// Flags are the command-line flags that set it.
	Flags []string
	// Envs are the environment variables that set it.
	Envs []string
	// Config are the internal/config fields that carry the configured value.
	Config []string
	// Malformed is what a value that does not parse does.
	Malformed Malformed
	// Zero is what a value of zero means.
	Zero Zero
	// OffWith is the ID of another row which, switched off, switches this one
	// off too: AUB-003's step is AUB-001's window (F-34, issue 958), and
	// RTC-007 follows the row it is the process partner of (issue 951).
	//
	// It names the row the code consults, which need not be the row an
	// operator sets to zero: RTC-007 is consulted where RTC-003's bucket
	// exists, and RTC-003's is derived from RTC-001's, which is the one a zero
	// switches off. The register does not record what a derived row is derived
	// from, so that second step is stated on the rows rather than followed.
	OffWith string
	// OffWithBy is the record that decided this row follows OffWith, when it
	// is a process partner following the per-key row it stands beside, which
	// is the one zero another row decides that passes without a finding
	// (INV-015). It is one of Decided; a row that records decisions about
	// anything else does not pass that way.
	OffWithBy string
	// AtCapacity is what happens when it is full.
	AtCapacity Capacity
	// Refusals are how it declines or ends, per method and era.
	Refusals []Refusal
	// Decided are the records that decided it, "ADR-0020", "issue 561".
	Decided []string
	// Findings are the findings it carries, "F-04"; [FindingIssue] names the
	// issue each is filed as.
	Findings []string
	// Sites are the symbols that decide, enforce, refuse or state it.
	Sites []Site
}

Decision is one requirement of the specification, declared once.

A row names its values by constant name, never by number: the numbers live in values.go alone, so a deliberate value change edits the constant and its frozen pin in the tests, two lines in one package (INV-021).

func Decisions

func Decisions() []Decision

Decisions returns every row of the register: one per requirement of the specification, eighty-two in all.

The rows are grouped by the question they answer (spec: The five questions), in the order the specification asks them (identify, admit, authorize, allow, end), with the per-request bounds last; inside a group they follow the order of their IDs. The table is built on each call and nothing holds it, so a binary that reaches no caller of this function carries none of it.

func Lookup

func Lookup(id string) (Decision, bool)

Lookup returns the row for a requirement id.

func (Decision) Carries

func (d Decision) Carries(finding string) bool

Carries reports whether the decision carries the finding id.

func (Decision) Disagrees

func (d Decision) Disagrees() bool

Disagrees reports whether the decision is class D: an allowance keyed on a requester whose reason is about a different unit than its key, so a tenant holding N keys holds N units of something its own reason says is one.

Only allowances can disagree: a rule or a lifetime keyed on the entry is the credential's own property. Units are compared as equivalence classes, so a reason about a credential agrees with a key on the entry. A reason about the process is met by a process partner beside the ceiling (INV-004).

func (Decision) RecordsDeparture

func (d Decision) RecordsDeparture(invariant string) bool

RecordsDeparture reports whether one of the decision's findings records a departure from invariant, "INV-004". It is what lets a row pass a rule it breaks today: the finding says so, and names the issue that will decide it.

type Disposition

type Disposition uint8

Disposition is what the register does with a decision.

const (
	// Valued decisions have their constants in values.go, and the sites alias
	// them.
	Valued Disposition = iota + 1
	// Ruled decisions are a predicate or a mapping, declared by symbol; nothing
	// moves.
	Ruled
	Promoted
	// Mechanism rows say how a decision is carried out. They choose no key and
	// no value.
	Mechanism
	// RequestBound rows are the per-request bounds, the RQB rows: declared so
	// that one list holds every requirement, and not owned.
	RequestBound
)

The dispositions.

type Era

type Era uint8

Era is a protocol revision a refusal applies in.

const (
	// EraAny is every era the refusal's methods exist in.
	EraAny Era = iota
	// EraLegacy is protocol 2025-11-25, with initialize and optional sessions.
	EraLegacy
	// EraModern is protocol 2026-07-28, stateless.
	EraModern
	// EraStdio is the stdio transport, where no HTTP status exists.
	EraStdio
)

The eras.

type Failure

type Failure struct {
	// Kind names the failure, "missing-credential".
	Kind string
	// Attributable says the failure is the caller's doing: no credential, or a
	// credential GitLab refused.
	Attributable bool
	// Charged says it is charged to the authentication budgets. Which of them
	// is its row's refusal's to say ([Refusal.Charged]): a missing credential
	// spends AUB-001 and AUB-002 and not AUB-003, which counts nothing for an
	// empty credential, and a rejected one spends all three. The gate holds the
	// two statements to each other (G7).
	Charged bool
	// Decision is the row whose refusal it is.
	Decision string
	// At is the function that returns the refusal, with Role Charge, the
	// charge helper in Call and the charged failures it returns in Count.
	At Site
	// Status is the refusal's HTTP status, which identifies the return
	// together with Prefix.
	Status int
	// Prefix is the refusal text's leading words where they fold to a
	// constant, and empty where they do not.
	Prefix string
}

Failure is one refusal an authentication path returns, and whether it is charged to the authentication budgets.

INV-007 used to be an absence: a failure was uncharged where nobody had written a charge call. The table states it instead, one row for every refusal the three admitting functions return, charged or not, and the gate binds each row to its return. So a charge added to a refusal the caller did not cause, or moved from one branch to its sibling, fails until the row says so, and ValidateFailures refuses the row unless the failure is the caller's doing.

func Failures

func Failures() []Failure

Failures returns every refusal the legacy gate and the OAuth bearer guard return: eight in the gate's resolve, seven in the guard's check and six in its classify. Five are charged, and each of them is a failure the caller caused; the rest are refused without a charge, because the credential was never judged or the refusal is about the request rather than the token.

A return that hands back another listed function's answer, as check does with classify's, is not a refusal of its own and has no row.

type Finding

type Finding struct {
	// ID is the finding's id in the specification, "F-04".
	ID string
	// Issue is the number of the issue it is filed as, in
	// https://github.com/jmrplens/gitlab-mcp-server/issues.
	Issue int
	// Invariants are the invariants it records a departure from (spec:
	// Invariants). A rule of [Validate] accepts a row that breaks an invariant
	// only through a finding listed for that invariant here.
	Invariants []string
}

Finding is one entry of the specification's findings register: a place where today's key disagrees with the tenant definition, or the answers disagree with each other, their reason or the protocol. None is fixed by the register; each is filed as an issue of its own.

func AllFindings

func AllFindings() []Finding

AllFindings returns the specification's thirty-five findings, in order, each with the issue it is filed as.

func (Finding) Records

func (f Finding) Records(invariant string) bool

Records reports whether the finding records a departure from invariant.

type Holdings

type Holdings interface {
	// OpenListenStreams is how many subscriptions/listen streams the entry
	// holds open, the count the per-credential listen ceiling draws on
	// (HLD-001). It is counted whether or not a ceiling is configured.
	OpenListenStreams() int64
	// Watchers is how many URIs the entry's subscription manager watches, or
	// zero on a surface that offers no subscriptions. It may take the
	// manager's lock, which is why [Busy] asks it only when no stream is open.
	Watchers() int
}

Holdings is what a pool entry holds that the pool cannot see: the work that keeps a credential busy while it sends no request (register row POL-003).

The server's per-credential state implements it; the counters, the manager and the idle sweep stay where they are. What lives here is which holdings make an entry busy: no longer counting one is an edit to the register and to its oracle test, and counting a third is that plus a method here, which the server's state then implements (an accessor in the layer, never a predicate).

type Key

type Key uint8

Key is what a declared decision is counted against: the unit one caller holds one of, or the process that no caller can multiply (spec: Keys).

A key is chosen from this vocabulary, never invented at the site. A decision that needs a key the vocabulary does not have adds it here, with what it costs a caller to mint and the evidence for that cost, which is how the mintable-key question gets answered once rather than per limit.

const (
	// KeyNone is no key. As a [Decision.StdioKey] it says the decision does
	// not exist on stdio.
	KeyNone Key = iota
	// KeyRequest is one request, argument set or watched URI (class Q).
	KeyRequest
	// KeyCredential is the raw token a request presents (IDN-001).
	KeyCredential
	// KeyEntry is one credential on one instance: the pool entry (IDN-002).
	KeyEntry
	// KeyOwner is the random name of one entry build (IDN-003).
	KeyOwner
	// KeyTenant is the pair (canonical instance URL, GitLab user id)
	// (spec: The tenant).
	KeyTenant
	// KeyVerified is the (instance, token) pair the OAuth identity cache is
	// keyed on (ADM-002, ADM-003, ADM-005). The rejected-token cache hashes
	// the same pair, and is [KeyRefused]'s (ADM-006).
	KeyVerified
	// KeyApplication is the OAuth application uid a token was issued to
	// (spec: The tenant).
	KeyApplication
	// KeySession is an SDK session id, recorded to an owner (IDN-010).
	KeySession
	// KeyAddress is the charged client address (IDN-004).
	KeyAddress
	// KeySource is the socket peer, with every header ignored (IDN-005).
	KeySource
	// KeyRefused is a credential GitLab refused, held as a digest: what
	// AUB-003 counts and what ADM-006 remembers.
	KeyRefused
	// KeyUnbound is every request no credential was bound to (IDN-007).
	KeyUnbound
	// KeyProcess is the running binary.
	KeyProcess
	// KeyDeployment is the operator's configuration.
	KeyDeployment
)

The keys (spec: Keys). The carrier and the shape are deliberately absent: nothing is counted against the carrier, and the shape keys a catalog cache that INV-010 governs rather than an allowance.

func Keys

func Keys() []Key

Keys returns every key of the vocabulary, KeyNone first.

func (Key) Axis

func (k Key) Axis() Axis

Axis reports which side of admission the key lives on.

func (Key) Derivation

func (k Key) Derivation() []Site

Derivation names the symbols that compute a value of the key today. They are identity mechanics rather than policy (a hash, a host canonicalization, an owner minted from the operating system's entropy), so they are declared here and not moved.

func (Key) Evidence

func (k Key) Evidence() string

Evidence is the key table's sentence for the key (spec: Keys), with the published pages it rests on. Validate quotes it when it refuses a share on the key.

func (Key) MintCost

func (k Key) MintCost() MintCost

MintCost reports what producing another value of the key costs a caller.

func (Key) Mintable

func (k Key) Mintable() bool

Mintable reports whether a caller can produce another value of the key at all. It is false only for the keys no caller controls: every key a request yields is mintable, the tenant included (spec: Two axes), and this is that rule as data.

func (Key) String

func (k Key) String() string

String names the key as the key table does (spec: Keys).

func (Key) Unit

func (k Key) Unit() Unit

Unit reports the equivalence class a reason about this key is compared in.

type Kind

type Kind uint8

Kind is the shape of a decision.

const (
	// Rule is a predicate or a mapping.
	Rule Kind = iota + 1
	// Ceiling is at most N held at once per key.
	Ceiling
	// Rate is a refill rate and a burst per key, or a pause keyed like one.
	Rate
	// Budget is events counted per key inside a window, then a block.
	Budget
	// Lifetime is how long an admission, a record, a build or a holding lasts.
	Lifetime
	// Bound is what one request may cost.
	Bound
	// Share is a portion promised to every key. [Validate] refuses it on any
	// key a caller can mint.
	Share
)

The kinds of decision.

func (Kind) IsAllowance

func (k Kind) IsAllowance() bool

IsAllowance reports whether the kind grants a key something to hold or spend: a ceiling, a rate, a budget or a share. Only an allowance can disagree with the tenant definition (see Decision.Disagrees).

type Malformed

type Malformed uint8

Malformed is what happens to a value that does not parse (INV-017).

const (
	// MalformedNone states none: a value no operator writes.
	MalformedNone Malformed = iota
	// RefuseStartup refuses to start.
	RefuseStartup
	// WarnKeepDefault warns and keeps the default.
	WarnKeepDefault
	// AcceptsAny is a switch every value of which means something (IDN-013:
	// anything but "off" enables it).
	AcceptsAny
)

The malformed-value policies.

type Meter

type Meter uint8

Meter is the bucket an MCP method is charged to, which also decides how its refusal is carried (register rows RTC-001 to RTC-004, and RTC-007 beside RTC-003).

The rate-limit middleware of internal/toolutil switches on it. The buckets themselves, their rates and bursts, the derivation of the completion and catalog buckets from the tool-call one, and the refusals stay where the requests are counted; what lives here is which method draws on which, so that metering one more method, or no longer metering one, is an edit to the register and to the oracle test beside it rather than to a switch in a layer.

const (
	// Unmetered methods are charged to no bucket (RTC-004): initialize,
	// resources/list, prompts/list and every method not named below. They
	// reach no upstream and cost little to answer, and metering something
	// cheap buys nothing and costs a concept.
	Unmetered Meter = iota
	// MeterToolResult is tools/call, charged to the entry's tool-call bucket
	// and refused as a successful result flagged with isError, so the model
	// receives a retryable diagnostic (RTC-001).
	MeterToolResult
	// MeterToolRPC is resources/read, resources/subscribe,
	// subscriptions/listen and prompts/get, charged to the same bucket as
	// tools/call because to GitLab a read is a read whichever MCP method asked
	// for it, and a limit that metered one door left the others open. Their
	// results carry no error flag, so each is refused with a JSON-RPC error
	// carrying [CodeTooManyRequests] (RTC-001).
	MeterToolRPC
	// MeterCatalog is tools/list, charged to the catalog bucket derived from
	// the tool-call one and refused with [CodeTooManyRequests]; the server's
	// own in-memory listings are exempt (RTC-003). It is charged first to the
	// bucket the whole process shares, in the tools it lists, which refuses
	// with the same code (RTC-007).
	MeterCatalog
	// MeterCompletion is completion/complete, charged to the completion
	// bucket derived from the tool-call one and refused with an empty
	// completion, never an error (RTC-002).
	MeterCompletion
)

The meters.

func MeterFor

func MeterFor(method string) Meter

MeterFor returns the bucket method is charged to.

The method is matched exactly as the SDK delivers it: no case folding and no trimming, so a method spelled any other way is Unmetered, as it was when the middleware compared the strings itself. It allocates nothing, and it is small enough to inline into the middleware that calls it once per request.

type MintCost

type MintCost uint8

MintCost is what producing one more value of a key costs a caller (spec: Keys, "Mint cost to the caller").

const (
	// NotMintable is a key no caller can produce another value of.
	NotMintable MintCost = iota
	// MintFree costs nothing: an invented string, a new request, a new session.
	MintFree
	// MintAddress costs another client address.
	MintAddress
	// MintCredential costs another credential of the same GitLab user.
	MintCredential
	// MintPrincipal costs another GitLab user, which on self-managed any user
	// with a personal project can create as a project bot (spec: Two axes).
	MintPrincipal
)

The mint costs, cheapest last among the mintable ones.

type Question

type Question uint8

Question is which of the five questions a decision answers (spec: The five questions). Every requirement answers exactly one.

const (
	// Identify is "who is this request?".
	Identify Question = iota + 1
	// Admit is "may this credential enter, and for how long?".
	Admit
	// Authorize is "what may it do?".
	Authorize
	// Allow is "what may it hold or spend?".
	Allow
	// End is "what happens when it exceeds, or when the server ends what it
	// held?".
	End
)

The five questions.

type Refusal

type Refusal struct {
	// Methods are MCP methods, or "http" for the gate, "startup" for the
	// process refusing to start, and "eviction" or "expiry" for the server
	// ending what the caller held (see [Carriages]).
	Methods []string
	// Era is the protocol revision the refusal applies in.
	Era Era
	// Channel is how it reaches the caller.
	Channel Channel
	// Code is the JSON-RPC code, zero when the channel carries none.
	Code int
	// Status is the HTTP status, on the Gate channel only.
	Status int
	// RetryAfter is where a gate refusal's Retry-After comes from.
	RetryAfter RetryAfter
	// Challenge says a gate refusal carries a WWW-Authenticate challenge. A
	// 401 always does (RFC 9110 section 15.5.2); a 403 does when it is about
	// the credential's scope, and not when it is about the Origin, the Host or
	// the instance a request named.
	Challenge bool
	// Prefix is the stable leading text clients match on (spec: Refusal
	// channels), empty when no text is carried or its start does not fold to a
	// constant.
	Prefix string
	// Answer is the one class of next action (INV-012).
	Answer Answer
	// Charged are the IDs of the Budget rows it is charged to; empty charges
	// nothing (INV-007).
	Charged []string
	// At is the code that holds its text.
	At Site
	// Via is the constructor whose literal carries status, code and headers
	// when At passes it the text; zero when the literal is in At.
	Via Site
}

Refusal is one way a decision declines a request or ends a holding, for a set of methods in one era.

type RetryAfter

type RetryAfter uint8

RetryAfter is where a gate refusal's Retry-After header takes its value.

const (
	// RetryAfterNone carries no Retry-After header.
	RetryAfterNone RetryAfter = iota
	// RetryAfterFixed is a register constant ([UpstreamRetryAfter]).
	RetryAfterFixed
	// RetryAfterUpstreamOrFixed is GitLab's own Retry-After, else the
	// constant (ADM-002).
	RetryAfterUpstreamOrFixed
	// RetryAfterLongestBlock is the longest block holding the request, at
	// least one second (AUB-001 to AUB-003).
	RetryAfterLongestBlock
)

The Retry-After sources.

type Role

type Role uint8

Role is what a Site is to its decision.

const (
	// Alias is a package-level const or var whose initializer is the register
	// constant [Site.Reads].
	Alias Role = iota + 1
	// Arg is a call argument: inside the function [Site.Name], the calls to
	// [Site.Call] pass the register constant at index [Site.Arg].
	Arg
	// Pin is a site that keeps its own literal, held equal to the register
	// constant [Site.Reads] (the second statements of issue 958's duplicated
	// defaults).
	Pin
	// Enforce is a function or var that applies the decision.
	Enforce
	// Refuse is a function, const or var that holds a refusal's text, or
	// builds its literal.
	Refuse
	// Reason is the declaration whose comment states the decision's reason.
	Reason
	// Charge is a function that charges an authentication budget.
	Charge
	// Derive is a symbol that computes a value of a key.
	Derive
)

The site roles.

type Site

type Site struct {
	// Pkg is the module-relative package directory, "cmd/server".
	Pkg string
	// Name is the symbol: "maxListenStreamsPerServer" for a package-level
	// const or var or a function, "listenLimits.middleware" for a method.
	Name string
	// Role is what the symbol is to the decision.
	Role Role
	// Reads is, for an Alias, Arg or Pin, the register constant it carries;
	// for an Enforce, a symbol its body must reference.
	Reads string
	// Call is, for an Arg, the callee; for a Charge, the helper counted.
	Call string
	// Arg is, for an Arg, the index of the argument; for an Alias whose
	// initializer is a composite literal, the index of the element.
	Arg int
	// Count is, for an Arg, the calls expected in the function; for a Charge,
	// the charged failures refused in the function.
	Count int
}

Site names a Go symbol, never a line, so that it survives edits above it.

type Source

type Source uint8

Source is where a decision's value comes from.

const (
	// SourceNone is a decision with no value, or a ceiling nothing bounds,
	// which checkProcessAbsent holds to a finding. HLD-010 was that ceiling
	// until issue 951 bounded the stateful sessions.
	SourceNone Source = iota
	// Constant is a value no operator can change.
	Constant
	// Configurable is a value with a flag and a GITLAB_MCP_ variable.
	Configurable
	// EnvOnly is a value only an environment variable changes.
	EnvOnly
	// OptionOnly is a value only a Go option changes, which no operator can
	// reach.
	OptionOnly
	// Derived is a value computed from another decision's, or from a limit
	// the process is started under (HLD-011, from its descriptor limit, and
	// HLD-010, from HLD-011's). No operator sets it, as none sets a constant.
	Derived
)

The value sources.

type Unit

type Unit uint8

Unit is the equivalence class a stated reason is compared in. A reason about a credential agrees with a key on the entry, because both are UnitCredential.

const (
	// UnitNone is the unit of a reason that names none.
	UnitNone Unit = iota
	// UnitRequest is one request.
	UnitRequest
	// UnitCredential is one credential, or anything held per credential.
	UnitCredential
	// UnitPrincipal is one GitLab user on one instance.
	UnitPrincipal
	// UnitSession is one session.
	UnitSession
	// UnitApplication is one OAuth application.
	UnitApplication
	// UnitAddress is one client address.
	UnitAddress
	// UnitProcess is the process.
	UnitProcess
)

The units a reason can be about.

type ViolationError

type ViolationError struct {
	// ID is the row, or, for a failure, its function and kind.
	ID string
	// Rule names the check, "share-on-mintable".
	Rule string
	// Invariant is what the check holds, "INV-003".
	Invariant string
	// Detail says what is wrong, in terms a reviewer can act on.
	Detail string
}

ViolationError is one way a declaration breaks an invariant of the specification.

func (*ViolationError) Error

func (v *ViolationError) Error() string

Error renders the violation as one line.

type Zero

type Zero uint8

Zero is what a value of zero means for a decision (INV-015).

const (
	// ZeroUnset has not been declared. [Validate] refuses it on a valued row.
	ZeroUnset Zero = iota
	// ZeroNotApplicable is a value no operator can set to zero.
	ZeroNotApplicable
	// ZeroRefused refuses startup.
	ZeroRefused
	// ZeroOff switches the decision off.
	ZeroOff
	// ZeroOffKeepsCount switches the ceiling off and keeps counting, because
	// another decision reads the count.
	ZeroOffKeepsCount
	// ZeroSelectsDefault replaces a zero with the default.
	ZeroSelectsDefault
)

The zero meanings.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL