Documentation
¶
Overview ¶
Package config defines the fold gateway configuration schema, loading, and validation. It mirrors fold's single-JSON-document configuration: upstreams, auth, policy, audit, routing, and server sections.
Index ¶
- func HostAllowed(patterns []string, host string) bool
- func Schema() []byte
- type Audit
- type AuditRetry
- type AuditSink
- type Auth
- type Budget
- type CircuitBreaker
- type ClientAuth
- type Config
- func (c *Config) AuthRequired() bool
- func (c *Config) ConsoleEnabled() bool
- func (c *Config) ConsoleOAuthIssuer() (*Issuer, error)
- func (c *Config) IntrospectionEnabled() bool
- func (c *Config) IntrospectionGroups() []string
- func (c *Config) KeepAlive() time.Duration
- func (c *Config) MCPPath() string
- func (c *Config) MaxBodyBytes() int64
- func (c *Config) NamespaceSeparator() string
- func (c *Config) PageSize() int
- func (c *Config) Passthrough() bool
- func (c *Config) SessionIdleTimeoutMs() int
- func (c *Config) Validate() error
- type Console
- type ConsoleOAuth
- type Discovery
- type EMAConfig
- type HealthCheck
- type Hook
- type Introspection
- type Issuer
- type Owner
- type Policy
- type PolicyAllow
- type PolicyRule
- type PolicySubjects
- type RateLimit
- type Routing
- type ServerSection
- type Tenant
- type Timeouts
- type Tracing
- type Upstream
- type UpstreamAuth
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func HostAllowed ¶ added in v1.1.0
HostAllowed reports whether host (possibly host:port) matches one of the patterns: an exact hostname, or "*.suffix" matching any subdomain.
Types ¶
type Audit ¶
type Audit struct {
Sinks []AuditSink `json:"sinks"`
}
Audit configures audit event emission.
type AuditRetry ¶ added in v1.9.0
type AuditRetry struct {
// MaxAttempts includes the first try (default 4, minimum 1).
MaxAttempts int `json:"maxAttempts,omitempty"`
// InitialBackoffMs is the first delay, doubling per attempt with jitter
// (default 500).
InitialBackoffMs int `json:"initialBackoffMs,omitempty"`
// MaxBackoffMs caps the delay (default 30000).
MaxBackoffMs int `json:"maxBackoffMs,omitempty"`
}
AuditRetry tunes redelivery for a failing sink.
type AuditSink ¶
type AuditSink struct {
Type string `json:"type"` // "stdout" | "webhook" | "file" | "otlp-logs"
URL string `json:"url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
// BearerSecretRef names an environment variable holding a bearer token
// for the sink, so the credential is not in the config document. Same
// convention as discovery's and the decision hook's.
//
// It exists because `headers` takes static values: a receiver that
// authenticates leaves the operator writing the token into the document
// itself, which is then the one part of a fold config that cannot be
// checked in, logged, or handed to anybody debugging a federation. The
// audit trail is exactly the sink most likely to need a credential.
BearerSecretRef string `json:"bearerSecretRef,omitempty"`
// Path is the file a "file" sink appends to, one JSON event per line.
Path string `json:"path,omitempty"`
// MaxSizeMb rotates the file once it exceeds this size (default 100).
// Rotation renames in place — audit.jsonl → audit.jsonl.1 → .2 — so a
// tail follows the live name.
MaxSizeMb int `json:"maxSizeMb,omitempty"`
// MaxFiles bounds how many rotated files are kept, oldest deleted first
// (default 5). A gateway that fills a disk with its own audit trail has
// found a novel way to stop serving.
MaxFiles int `json:"maxFiles,omitempty"`
// Retry governs delivery for sinks that can fail transiently — today the
// webhook. Absent means the defaults, not "no retry": a receiver
// restarting is the ordinary case, and losing the events it was down for
// is the thing audit cannot afford.
Retry *AuditRetry `json:"retry,omitempty"`
// DeadLetterPath is where events go when delivery is finally given up
// on, one JSON event per line. Absent, exhausted events are counted and
// logged but not kept — set it wherever the audit trail is load-bearing.
DeadLetterPath string `json:"deadLetterPath,omitempty"`
}
AuditSink is one audit destination.
type Auth ¶
type Auth struct {
Mode string `json:"mode,omitempty"` // "disabled" (default) | "required"
Resource string `json:"resource,omitempty"`
Issuers []Issuer `json:"issuers,omitempty"`
// EMA enables Enterprise-Managed Authorization: fold's embedded
// one-grant token endpoint exchanging enterprise-IdP ID-JAGs for
// fold-signed access tokens.
EMA *EMAConfig `json:"ema,omitempty"`
}
Auth configures the gateway's OAuth 2.0 resource server.
type Budget ¶ added in v1.7.0
type Budget struct {
// Period is the window: "hour", "day", or "month" (default). Boundaries
// are UTC, so a fleet spanning zones agrees on which month it is.
Period string `json:"period,omitempty"`
// UpstreamCalls is the allowance. Absent or <= 0 means no budget.
UpstreamCalls int64 `json:"upstreamCalls,omitempty"`
}
Budget caps total consumption over a calendar-aligned period that resets at the boundary — the "how much this month" question a rate limit cannot answer, since a sliding window forgets rather than accumulates.
The unit is upstream invocations, not downstream requests: one tools/list fans out to every upstream in the federation, so counting client requests would price a list the same as a ping. See docs/design-consumption.md.
func (*Budget) Allowance ¶ added in v1.7.0
Allowance returns the configured allowance, or 0 for "no budget".
func (*Budget) ResolvedPeriod ¶ added in v1.7.0
ResolvedPeriod returns the configured period, defaulting to "month".
type CircuitBreaker ¶
type CircuitBreaker struct {
FailureThreshold int `json:"failureThreshold,omitempty"` // default 5
HalfOpenAfterMs int `json:"halfOpenAfterMs,omitempty"` // default 30000
}
CircuitBreaker short-circuits an unhealthy upstream.
type ClientAuth ¶
type ClientAuth struct {
Type string `json:"type"` // "client_secret_post" | "client_secret_basic"
SecretRef string `json:"secretRef"`
}
ClientAuth describes how the gateway authenticates to a token endpoint.
type Config ¶
type Config struct {
Upstreams []Upstream `json:"upstreams"`
Auth *Auth `json:"auth,omitempty"`
Policy *Policy `json:"policy,omitempty"`
Audit *Audit `json:"audit,omitempty"`
Routing *Routing `json:"routing,omitempty"`
Server *ServerSection `json:"server,omitempty"`
Tracing *Tracing `json:"tracing,omitempty"`
Discovery *Discovery `json:"discovery,omitempty"`
// Tenants group principals for governance. Reloadable: tenants change
// when a customer signs up, and requiring a restart for that would push
// operators toward a write control plane fold does not have.
Tenants []Tenant `json:"tenants,omitempty"`
// Hook is the external decision endpoint — fold's one out-of-process
// seam, and its answer to the plugin runtime it declines. Absent (the
// default) means nothing on the request path changes.
Hook *Hook `json:"hook,omitempty"`
}
Config is the root configuration document.
func (*Config) AuthRequired ¶
AuthRequired reports whether gateway authentication is enabled.
func (*Config) ConsoleEnabled ¶ added in v1.2.0
ConsoleEnabled reports whether the read-only console page is served.
func (*Config) ConsoleOAuthIssuer ¶ added in v1.4.1
ConsoleOAuthIssuer resolves the trusted issuer the console's PKCE sign-in uses: console.oauth.issuer when set (which must name a configured direct-mode issuer), else the first direct-mode issuer. Errors when console.oauth is absent or no direct issuer qualifies.
func (*Config) IntrospectionEnabled ¶ added in v1.9.0
IntrospectionEnabled reports whether the read-only APIs (/api/federation, /api/auth-hint) are served.
func (*Config) IntrospectionGroups ¶ added in v1.9.0
IntrospectionGroups returns the viewer allowlist for /api/federation. Empty means any valid principal may read it.
func (*Config) KeepAlive ¶ added in v1.15.0
KeepAlive resolves server.keepAliveMs. Zero — the default — disables the ping loop entirely.
func (*Config) MaxBodyBytes ¶
MaxBodyBytes returns the request body cap (default 1 MiB).
func (*Config) NamespaceSeparator ¶
NamespaceSeparator returns the configured separator (default "__").
func (*Config) PageSize ¶ added in v0.5.0
PageSize returns the per-page bound for federated list results: the configured value, defaulting to 200; 0 means pagination is disabled (configured negative).
func (*Config) Passthrough ¶
Passthrough reports whether the gateway runs in zero-copy passthrough mode (a single upstream with no namespace).
func (*Config) SessionIdleTimeoutMs ¶ added in v1.11.0
SessionIdleTimeoutMs returns how long a downstream MCP session may sit idle before the gateway closes it, in milliseconds. Default 30 minutes; 0 — from a negative config value — means sessions never expire.
type Console ¶ added in v1.2.0
type Console struct {
Enabled bool `json:"enabled"`
// OAuth lets the console sign users in with Authorization Code +
// PKCE instead of a pasted token. Requires auth.mode "required".
OAuth *ConsoleOAuth `json:"oauth,omitempty"`
}
Console configures the read-only console page. Like the rest of the server section it is construction-wired: changing it requires a restart, not a hot reload.
type ConsoleOAuth ¶ added in v1.4.1
type ConsoleOAuth struct {
// ClientID is the public client id registered at the IdP for the
// console. Client ids are not secrets — every SPA ships one.
ClientID string `json:"clientId"`
// Issuer selects which trusted issuer the console signs in against.
// Must match a configured auth issuer with mode "direct" (an
// "exchange" issuer's tokens are ID-JAGs, not presentable access
// tokens). Default: the first direct issuer.
Issuer string `json:"issuer,omitempty"`
// Scopes requested at authorization (default none beyond what the
// IdP grants implicitly; the resource/audience comes from
// auth.resource via RFC 8707).
Scopes []string `json:"scopes,omitempty"`
}
ConsoleOAuth configures the console's browser sign-in. The console is a public OAuth client: no secret exists, PKCE is the proof. Register the gateway's console URL ({origin}/console/) as the redirect URI at the IdP.
type Discovery ¶ added in v0.6.0
type Discovery struct {
// URL of the discovery document. It decides where traffic routes and
// where upstream credentials attach, so it must use https (loopback
// exempt for development).
URL string `json:"url"`
IntervalMs int `json:"intervalMs,omitempty"` // poll interval; default 30000
// BearerSecretRef names an environment variable whose value is sent as
// a Bearer token when fetching the document.
BearerSecretRef string `json:"bearerSecretRef,omitempty"`
// AllowedAuthStrategies restricts the credential strategies discovered
// upstreams may carry ("none" needs no listing). Whoever controls the
// discovery source controls both an upstream's secretRef names and its
// destination URL — an unrestricted source can point gateway-held
// secrets (or, via passthrough, caller tokens) at any endpoint. Absent
// → unrestricted; present → a document whose upstream carries any other
// strategy is rejected whole, keeping the last good set.
AllowedAuthStrategies []string `json:"allowedAuthStrategies,omitempty"`
// AllowedSecretRefs restricts which environment variables discovered
// upstreams may name in secretRef fields (upstream auth and client
// auth). Absent → unrestricted; present → any other reference rejects
// the document whole.
AllowedSecretRefs []string `json:"allowedSecretRefs,omitempty"`
// AllowedCredentialHosts restricts where a *credentialed* discovered
// upstream may send secrets: both its endpoint hosts and its
// tokenEndpoint host must match. Naming a secret is only half the
// exposure — the destination is the other half, and a discovery source
// chooses both. Entries are hostnames, optionally with a leading "*."
// wildcard ("*.svc.cluster.local"); ports are ignored. Absent →
// unrestricted; present → a violating document is rejected whole.
// Upstreams with no credentials (strategy none/absent) are unaffected.
AllowedCredentialHosts []string `json:"allowedCredentialHosts,omitempty"`
// MinHealthCheckIntervalMs floors healthCheck.intervalMs on discovered
// upstreams (default 1000). A discovery source could otherwise turn the
// gateway into a probe flood against a host of its choosing.
MinHealthCheckIntervalMs int `json:"minHealthCheckIntervalMs,omitempty"`
}
Discovery enables dynamic upstream discovery: fold polls url for a JSON document {"upstreams": [...]} (same schema as the static upstreams section) and hot-swaps the discovered set into the federation alongside the statically configured upstreams. A document that fails validation — including id or namespace collisions with static upstreams — is rejected whole and the last good set keeps serving.
func (*Discovery) MinHealthCheckIntervalResolved ¶ added in v1.1.0
MinHealthCheckIntervalResolved returns the floor applied to discovered upstreams' health-probe interval (default 1000 ms).
type EMAConfig ¶
type EMAConfig struct {
// IdpIssuer is the enterprise IdP that issues ID-JAGs.
IdpIssuer string `json:"idpIssuer"`
IdpJWKSURI string `json:"idpJwksUri,omitempty"` // default {idpIssuer}/.well-known/jwks.json
// SigningKeyRef names an environment variable holding fold's ES256
// private key (PKCS#8 PEM) for signing minted access tokens.
SigningKeyRef string `json:"signingKeyRef"`
TokenTTLSec int `json:"tokenTtlSec,omitempty"` // minted-token lifetime; default 600
// TokenRateLimitPerMinute caps the unauthenticated /oauth/token
// endpoint (anti-amplification). Default 600.
TokenRateLimitPerMinute int `json:"tokenRateLimitPerMinute,omitempty"`
}
EMAConfig is the Enterprise-Managed Authorization section.
func (*EMAConfig) ResolvedTokenRateLimit ¶
ResolvedTokenRateLimit returns the /oauth/token cap (default 600/min).
func (*EMAConfig) ResolvedTokenTTLSec ¶
ResolvedTokenTTLSec returns the minted-token lifetime (default 600).
type HealthCheck ¶ added in v0.5.0
type HealthCheck struct {
IntervalMs int `json:"intervalMs"`
}
HealthCheck configures active endpoint probing.
type Hook ¶ added in v1.12.0
type Hook struct {
// URL receives a JSON POST per inspected invocation.
URL string `json:"url"`
// TimeoutMs bounds one decision. Required, with no default: a hook
// without a bound is a gateway without one, and a slow hook is more
// dangerous than a broken one because failing open turns it into an
// invisible bypass.
TimeoutMs int `json:"timeoutMs"`
// OnError is the decision when the hook times out, refuses the
// connection, answers non-2xx, or returns something unparseable:
// "allow" or "deny". Required, with no default — both readings are
// legitimate (compliance wants traffic to stop when inspection stops;
// availability-first wants the gateway to keep serving), so fold refuses
// to guess on an operator's behalf and refuses to start without the
// choice.
OnError string `json:"onError"`
// Stages selects what is inspected: "ingress" (the invocation and its
// arguments), "egress" (the result), and "serverInitiated" (what an
// upstream asks of the caller's client). Nothing runs unless named.
//
// The two are not interchangeable. By egress the upstream has already
// acted, so a denial there withholds the disclosure and not the effect:
// egress is a data-loss control, and stopping an action means refusing it
// at ingress.
Stages []string `json:"stages,omitempty"`
// Methods limits inspection to specific MCP methods. Absent means every
// named invocation policy governs.
Methods []string `json:"methods,omitempty"`
// Headers are sent with each decision request, verbatim — the same shape
// an audit webhook sink takes. Static values only.
Headers map[string]string `json:"headers,omitempty"`
// BearerSecretRef names an environment variable holding a bearer token
// for the hook endpoint, so the credential is not in the config
// document. Same convention as discovery's.
BearerSecretRef string `json:"bearerSecretRef,omitempty"`
}
Hook configures the external decision endpoint. It decides, and cannot rewrite: fold either forwards a request verbatim or refuses it, which is what keeps the invisibility rule intact. See docs/design-decision-hook.md.
type Introspection ¶ added in v1.9.0
type Introspection struct {
Enabled bool `json:"enabled"`
// Groups restricts who may read /api/federation: when set, an
// authenticated principal must carry at least one of these groups
// (per its issuer's groupsClaim) or the API answers an audited 403.
// Requires auth.mode "required". Absent → any valid principal may
// read. Console static assets are never gated — they carry no data.
// Group names are only unique within an issuer (the same caveat as
// policy rules), so keep the list meaningful across every trusted
// issuer.
Groups []string `json:"groups,omitempty"`
}
Introspection configures the read-only HTTP APIs. Like the rest of the server section it is construction-wired: changing it requires a restart, not a hot reload.
type Issuer ¶
type Issuer struct {
Issuer string `json:"issuer"`
JWKSURI string `json:"jwksUri,omitempty"` // default {issuer}/.well-known/jwks.json
GroupsClaim string `json:"groupsClaim,omitempty"` // default "groups"
// Mode is "direct" (default): clients present this issuer's access
// tokens straight to fold — or "exchange": this issuer's tokens are
// ID-JAGs redeemable only at the EMA token endpoint; presenting one
// directly as a fold token is rejected.
Mode string `json:"mode,omitempty"`
}
Issuer is a trusted token issuer.
type Owner ¶
type Owner struct {
Org string `json:"org,omitempty"`
Team string `json:"team,omitempty"`
Contact string `json:"contact,omitempty"`
}
Owner records which organization runs an upstream. Surfaces in audit and health.
type Policy ¶
type Policy struct {
DefaultDecision string `json:"defaultDecision,omitempty"` // "deny" (default) | "allow"
Rules []PolicyRule `json:"rules,omitempty"`
// ServerInitiatedDecision governs the reverse direction — the
// sampling/createMessage and elicitation/create requests an upstream
// makes of the caller's client over a bridged session. "allow" (the
// default) | "deny".
//
// It is a separate knob from DefaultDecision, and defaults to allow, for
// a compatibility reason rather than a security one: this traffic flows
// today in every deployment, including deny-by-default ones, so folding
// it under the existing field would break working installs on upgrade.
// Production deployments should set it to "deny" and grant the reverse
// methods explicitly — see docs/design-server-initiated.md.
ServerInitiatedDecision string `json:"serverInitiatedDecision,omitempty"`
}
Policy is the deny-by-default allowlist engine configuration.
type PolicyAllow ¶
type PolicyAllow struct {
Server string `json:"server"`
Methods []string `json:"methods,omitempty"` // omit → all methods
Names []string `json:"names,omitempty"` // omit → all names
// Args constrains the invocation's arguments: a map of dotted JSON path
// to required value, all of which must match. It is the difference
// between "may call deploy" and "may call deploy against staging".
//
// Values compare type-exactly, like subject claims — "1" and 1 are
// different, because a lenient comparison silently widens a grant. Paths
// have no wildcards and no array indexing.
//
// A constrained clause makes a tool **visible but conditionally
// callable**: there are no arguments at list time, so this cannot filter
// a list. An operator who needs the stronger guarantee grants by name.
Args map[string]any `json:"args,omitempty"`
// ToolKind gates on the MCP tool annotations an upstream publishes:
// "readOnly" requires readOnlyHint, "nonDestructive" additionally admits
// tools whose destructiveHint is false. It is what lets a rule say "read
// anything, write nothing" without naming every tool.
//
// **A hygiene control, not a security boundary.** The annotations are
// declared by the very server being gated, so an upstream that labels
// delete_everything as read-only is believed. Use it for federations your
// organization operates; against an upstream you do not control, the
// boundary is Names.
//
// The MCP spec's defaults apply as written and are fail-safe:
// readOnlyHint defaults to false, destructiveHint to true, so an
// unannotated tool is neither read-only nor non-destructive. A tool whose
// annotations fold cannot establish is denied — which includes upstreams
// whose list caching is disabled because their credential is
// caller-derived, so toolKind and passthrough/token-exchange do not
// compose.
ToolKind string `json:"toolKind,omitempty"`
}
PolicyAllow grants methods/names on one upstream. Names support "*" globs.
type PolicyRule ¶
type PolicyRule struct {
ID string `json:"id"`
Subjects *PolicySubjects `json:"subjects,omitempty"` // omit → any principal
Allow []PolicyAllow `json:"allow,omitempty"`
// Deny is shaped exactly like Allow and matches the same way, but any
// match refuses the invocation **regardless of rule order** — an explicit
// deny is not overridable by an allow, the way it works in IAM and in
// firewalls. Order-independence is the point: a rule whose correctness
// depends on where it was pasted will eventually be pasted in the wrong
// place.
//
// A rule may carry allow, deny, or both, and at least one of them.
Deny []PolicyAllow `json:"deny,omitempty"`
// MaxItems caps how many list items this rule may make visible in one
// response — a guardrail against handing an agent a thousand tools, which
// is context an operator pays for on every turn.
//
// It is a bound, not a curation: fold drops whatever falls past the cap in
// merge order, because it has no notion of which tools matter and
// inventing one would be the semantic tool selection this project
// declines. A truncated list says so, in the result _meta, the audit
// event, and a metric — a cap that hid capability silently would be worse
// than no cap. Absent or 0 means no limit.
MaxItems int `json:"maxItems,omitempty"`
}
PolicyRule allows a set of principals a set of invocations.
type PolicySubjects ¶
type PolicySubjects struct {
Groups []string `json:"groups,omitempty"`
Subs []string `json:"subs,omitempty"`
Issuers []string `json:"issuers,omitempty"`
// Claims gates the rule on verified token claims (attribute-based
// access control): every entry must match — the claim equals the value,
// or, when the token claim is an array, contains it. Values must be
// JSON scalars (string, number, bool). Combines with subs/groups as an
// additional requirement, like issuers.
Claims map[string]any `json:"claims,omitempty"`
// Scopes gates the rule on the OAuth scopes the token carries: the
// principal must hold *every* scope named. That is conjunctive, unlike
// groups and subs, which are alternatives — a scope is an authorization
// the token was granted rather than an identity it has, so "requires
// read and write" is the only reading of a list of them that matches
// what an operator writing it means.
//
// It exists as its own field rather than as a claims entry because the
// standard spelling cannot be matched by the claims matcher: RFC 6749
// makes "scope" a space-delimited string, so `claims: {"scope": "write"}`
// does not match a token carrying "read write". Scopes are read from
// "scope" or "scp", as a string or an array — see auth.ScopesFromClaims.
Scopes []string `json:"scopes,omitempty"`
}
PolicySubjects matches principals by group membership and/or subject, optionally scoped to specific token issuers and gated on token claims. Because subjects, groups, and claims are only meaningful within an issuer, scope rules to an issuer whenever more than one is trusted.
type RateLimit ¶
type RateLimit struct {
RequestsPerMinute int `json:"requestsPerMinute"`
// PerPrincipalPerMinute additionally caps each authenticated principal
// on its own bucket, so one tenant's flood cannot consume the shared
// budget and starve every other tenant. Server-level only; requires
// auth (anonymous callers are governed by the global budget alone).
PerPrincipalPerMinute int `json:"perPrincipalPerMinute,omitempty"`
}
RateLimit is a sliding-window request budget over the trailing minute.
type Routing ¶
type Routing struct {
NamespaceSeparator string `json:"namespaceSeparator,omitempty"` // default "__"
// PageSize bounds federated list results (tools/prompts/resources/
// templates/tasks) per page. 0 uses the default (200); negative disables
// pagination and returns the full merged list as a single page.
PageSize int `json:"pageSize,omitempty"`
}
Routing tunes name federation.
type ServerSection ¶
type ServerSection struct {
MCPPath string `json:"mcpPath,omitempty"` // default "/mcp"
AllowedHosts []string `json:"allowedHosts,omitempty"` // default localhost set; ["*"] disables
RateLimit *RateLimit `json:"rateLimit,omitempty"` // global, across all upstreams
// Budget caps total consumption across every upstream over a calendar
// period. Like the rest of this section it is construction-wired: Reload
// rejects a change to it, so a budget cannot be widened by editing config
// under a running gateway.
Budget *Budget `json:"budget,omitempty"`
// MaxBodyBytes caps request body size (413 beyond it), bounding the
// memory one request can pin. Default 1 MiB.
MaxBodyBytes int64 `json:"maxBodyBytes,omitempty"`
// KeepAliveMs makes the gateway ping each connected client on an
// interval, so a long-lived stream keeps carrying bytes.
//
// Absent (0) it is off, which is what fold has always done. The SDK's
// own note is that it expects clients to ping if they want a session
// kept alive — reasonable for a server, and less so for a gateway,
// which is typically the one thing in the path an operator controls
// while the idle timeout cutting the stream belongs to a load balancer
// they configured separately. A cut stream is survivable (both ends
// reconnect) but it is reconnect churn and a window where notifications
// are not being delivered.
//
// The peer that fails to answer has its session closed, which is the
// point — an unanswered ping is how a dead client is noticed — so this
// is not free to turn on: set it longer than the round trip to your
// slowest legitimate client, not merely shorter than the balancer's
// idle timeout.
KeepAliveMs int `json:"keepAliveMs,omitempty"`
// SessionIdleTimeoutMs closes a downstream MCP session after this long
// without a request from its client. Ending a session with DELETE is
// optional in the protocol and clients routinely reconnect without it;
// an unexpired session pins gateway memory — and the upstream
// subscriptions it holds — forever. Default 30 minutes; negative
// disables expiry (the pre-1.11 behavior). Construction-wired like the
// rest of this section: Reload rejects a change to it.
SessionIdleTimeoutMs int `json:"sessionIdleTimeoutMs,omitempty"`
// RedisURL shares cache, rate-limit, and circuit-breaker state across
// gateway instances (redis:// URL). Defaults to the REDIS_URL
// environment variable; absent → in-process state.
RedisURL string `json:"redisUrl,omitempty"`
// MetricsAddr moves /metrics to its own listener ("host:port", or
// ":9090" for every interface). Absent — the default — leaves /metrics on
// the main mux, behind the same Host allowlist as everything else.
//
// Why moving it is the safer arrangement rather than exempting the path:
// a scrape carries upstream ids, namespaces, tenant ids, and the endpoint
// URLs of multi-endpoint upstreams. On the main mux, DNS-rebinding checks
// are what keep a browser from reading that — and the price is that a
// scraper arriving under any other name is answered 403, which is why a
// ServiceMonitor cannot scrape a pod IP. A separate listener settles
// both: it is not an origin a victim's browser can be steered to, so it
// needs no Host allowlist, and a scraper may address it however it likes.
// Bind it to an internal interface and keep it off the public network —
// that network scope is what protects it.
//
// Construction-wired like the rest of this section: Reload rejects a
// change to it.
MetricsAddr string `json:"metricsAddr,omitempty"`
// Introspection serves the gateway's read-only HTTP APIs: /api/federation
// (the federation snapshot — upstream health, policy shape, audit sinks,
// discovery status, the viewer's tenant governance) and /api/auth-hint
// (the unauthenticated sign-in hint). Separate from `console` because the
// API and the page are separate surfaces: an operator may want the data
// without serving a browser page at all, and the API stopped being the
// console's private detail when the console became separately versioned.
// Off by default. See docs/design-console.md.
//
// Construction-wired like the rest of this section: Reload rejects a
// change to it.
Introspection *Introspection `json:"introspection,omitempty"`
// Console enables the read-only fold console page at /console: an
// observability dashboard plus an MCP test console that talks to the
// gateway's own /mcp endpoint (fully governed — policy, rate limits,
// and audit apply to console traffic like any other client's). The page
// renders what /api/federation reports, so it requires
// introspection.enabled. Off by default. See docs/design-console.md.
Console *Console `json:"console,omitempty"`
}
ServerSection configures the gateway's own HTTP server.
type Tenant ¶ added in v1.8.0
type Tenant struct {
ID string `json:"id"`
// Subjects selects the principals in this tenant, using the same shape
// policy rules use. A principal belongs to at most one tenant; matching
// more than one is a configuration error, refused at request time rather
// than resolved by precedence.
Subjects *PolicySubjects `json:"subjects,omitempty"`
// Budget caps this tenant's consumption over a calendar period — the
// dimension a per-upstream or server-wide budget cannot express.
Budget *Budget `json:"budget,omitempty"`
// RateLimit gives the tenant one shared bucket. Distinct from
// server.rateLimit.perPrincipalPerMinute, which gives each *person* a
// bucket: ten agents on one team get ten allowances there and one here.
RateLimit *RateLimit `json:"rateLimit,omitempty"`
// Upstreams optionally bounds which upstreams this tenant may see at
// all, by id. Evaluated before policy, which remains the authority on
// what may be invoked. Empty means every upstream.
Upstreams []string `json:"upstreams,omitempty"`
}
Tenant names a set of principals and the governance that applies to them as a group. It is a label on identity, resolved from claims the IdP already asserts — never presented alongside a token, never a trust anchor. A tenant groups principals; it does not authenticate them. See docs/design-tenancy.md.
type Timeouts ¶
type Timeouts struct {
ConnectMs int `json:"connectMs,omitempty"` // default 5000
RequestMs int `json:"requestMs,omitempty"` // default 60000
// StreamIdleMs, when set, cuts the upstream's standalone SSE stream
// after this much silence so a wedged stream (TCP alive, no bytes) is
// retried instead of held forever; the SDK client reconnects it. Off by
// default (0): the client's reconnect budget only replenishes when
// events actually arrive, so an idle bound against an upstream that
// legitimately never notifies would eventually kill its session. Set it
// only for upstreams that emit notifications or SSE keepalives.
StreamIdleMs int `json:"streamIdleMs,omitempty"`
}
Timeouts bounds upstream I/O.
type Tracing ¶ added in v0.5.0
type Tracing struct {
// OTLPEndpoint is the collector's OTLP/HTTP base URL, e.g.
// "http://otel-collector:4318". Plain http is allowed — collectors are
// commonly cluster-local sidecars.
OTLPEndpoint string `json:"otlpEndpoint"`
ServiceName string `json:"serviceName,omitempty"` // default "fold"
// SampleRatio samples traces fold roots itself at this rate (0 < r <= 1,
// default 1). Parent-based: callers that sampled their trace stay
// sampled regardless.
SampleRatio float64 `json:"sampleRatio,omitempty"`
// RecordPrincipal stamps the verified principal's subject on server
// spans as `enduser.id`. Off by default (changed in v1.11 — earlier
// versions always stamped it): the subject is personal data, and trace
// backends commonly have broader access than the audit trail, which
// carries the same identity under audit-grade access instead.
RecordPrincipal bool `json:"recordPrincipal,omitempty"`
}
Tracing enables first-party OpenTelemetry spans (one server span per MCP request, one client span per upstream call), exported over OTLP/HTTP. Absent → fold only propagates the caller's W3C trace context.
type Upstream ¶
type Upstream struct {
ID string `json:"id"`
URL string `json:"url,omitempty"`
// URLs lists multiple equivalent replicas of this upstream. The gateway
// load-balances new sessions across them round-robin, fails over to the
// next endpoint when one refuses connections, and rests a failed endpoint
// for the circuit breaker's halfOpenAfterMs before retrying it. Exactly
// one of url / urls must be set.
URLs []string `json:"urls,omitempty"`
Namespace string `json:"namespace,omitempty"`
// Protocol selects the era of the upstream connection. "session" (the
// default) negotiates the sessionful handshake, which is required for
// server-initiated traffic (sampling, elicitation, logging, progress,
// resource-update notifications) to bridge through the gateway.
// "auto" lets the SDK prefer the newest protocol (2026-07-28
// stateless), which cannot carry server-initiated requests.
Protocol string `json:"protocol,omitempty"`
Owner *Owner `json:"owner,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Auth *UpstreamAuth `json:"auth,omitempty"`
Timeouts *Timeouts `json:"timeouts,omitempty"`
CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"`
RateLimit *RateLimit `json:"rateLimit,omitempty"`
// Budget caps total consumption of this upstream over a calendar period.
// Distinct from RateLimit: a rate limit smooths a burst and forgets it,
// a budget accumulates until the period rolls over.
Budget *Budget `json:"budget,omitempty"`
// HealthCheck enables active endpoint probing: every intervalMs the
// gateway connects to each endpoint, ejecting dead replicas from the
// balancer before client traffic hits them and restoring recovered ones
// without waiting for a live-request retry. Absent → passive health only
// (connect failures eject, cooldown restores).
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
// CacheTTLMs bounds staleness of cached list results (tools/prompts/
// resources) for this upstream. 0 uses the gateway default (30s);
// negative disables caching.
CacheTTLMs int `json:"cacheTtlMs,omitempty"`
// MaxResponseBytes bounds a single response from this upstream. 0 uses
// the gateway default (64 MiB); negative disables the bound.
//
// The inbound direction has had server.maxBodyBytes since v1, and every
// other thing fold fetches — tokens, JWKS, the discovery document, the
// decision hook — is already bounded. The upstream data path was the one
// place with no limit at all, which made a buggy or hostile upstream able
// to spend the gateway's memory: an oversized list is decoded, cached,
// and (with Redis configured) pushed into shared fleet state. It is the
// internal/bounded rule applied to bytes rather than to keys.
//
// The bound refuses rather than truncates. A shortened body would be the
// response rewriting fold declines, so an upstream that exceeds it is
// reported as unavailable (-31041) — which is what it is.
MaxResponseBytes int `json:"maxResponseBytes,omitempty"`
// PinDefinitions notices when this upstream changes what it advertises —
// a tool's description, schema, or annotations rewritten after the
// federation was approved. "off" (the default) | "warn".
//
// It is a comparison, not an inspection: fold records the digest of each
// definition it serves and reports a difference. It never judges the
// content, which is why this is not the inline scanning fold declines.
// See docs/design-definition-pinning.md.
PinDefinitions string `json:"pinDefinitions,omitempty"`
}
Upstream describes one MCP server folded into the gateway.
type UpstreamAuth ¶
type UpstreamAuth struct {
// Strategy is one of "none", "static", "passthrough",
// "client-credentials", or "token-exchange".
Strategy string `json:"strategy"`
// static
SecretRef string `json:"secretRef,omitempty"` // env var holding the secret
Header string `json:"header,omitempty"` // default "Authorization"
Scheme string `json:"scheme,omitempty"` // default "Bearer" when header is Authorization
// client-credentials / token-exchange
TokenEndpoint string `json:"tokenEndpoint,omitempty"`
ClientID string `json:"clientId,omitempty"`
ClientAuth *ClientAuth `json:"clientAuth,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Resource string `json:"resource,omitempty"` // client-credentials (RFC 8707)
Audience string `json:"audience,omitempty"` // token-exchange (RFC 8693)
}
UpstreamAuth selects the credential strategy used toward an upstream.