Skip to content

API reference

Every field of every actions-gateway.com/v2beta1 kind, generated from the Go types the CRD schemas are generated from. For what the kinds are and how they fit together, start at the reference overview; for the deprecated v1alpha1 and v2alpha1 versions, see Deprecations & the v2.0.0 removal.

Packages

actions-gateway.com/v2beta1

Package v2beta1 contains the API Schema definitions for the actions-gateway.com v2beta1 API group: all five v2 kinds — the GMC-reconciled ActionsGateway (control) and EgressProxy (data) kinds, and the AGC-reconciled RunnerSet (control) and RunnerTemplate / ClusterRunnerTemplate (data) kinds — plus their shared types.

v2beta1 is the graduation of the v2alpha1 API (Q74). It is the storage version and the conversion hub: the five kinds implement conversion.Hub here, while v2alpha1 implements conversion.Convertible (ConvertTo/ConvertFrom) as the spoke. v2beta1 is served beside v2alpha1 during the coexistence window so a tenant can roll forward at its own pace (and the gag-migrate Classic on-ramp keeps landing on v2alpha1); the two versions round-trip losslessly through the conversion webhook.

The shape is identical to v2alpha1 with one deliberate exception: v2beta1 is ScaleSet-only, so RunnerSet drops the transitional acquisitionProtocol selector and the classic-only maxListeners knob (Q264 §5a-U7/U8). Those two v2alpha1-only fields survive a v2alpha1→v2beta1→v2alpha1 round-trip via an annotation carried on the v2beta1 object (see api/v2alpha1/conversion.go), so a coexistence-era Classic or ScaleSet set is never silently re-protocol'd.

These kinds live in a single neutral module (github.com/actions-gateway/github-actions-gateway/api) that both the GMC and AGC controller modules import. The AGC's RunnerSet reconciler must read the GMC-group ActionsGateway (gatewayRef) and EgressProxy (proxyRef), but the GMC module already imports the AGC module to build RunnerSet CRs; co-locating the v2 kinds here breaks that would-be module dependency cycle without either controller module importing the other's API package (the neutral api/ module resolves the GMC↔AGC cycle; see docs/development/go-workspaces.md).

Resource Types

AGCVerticalAutoscaling

AGCVerticalAutoscaling configures the managed VerticalPodAutoscaler the GMC stamps next to this gateway's AGC Deployment (Q360). Its presence is the opt-in; there is no enabled flag, so removing the block removes the autoscaler.

Precedence over agcResources

Both this block and agcResources influence the AGC container's resources, so the division is fixed and explicit rather than "last writer wins" (resolved at reconcile, not rejected at admission — the combination is coherent, not contradictory):

  • agcResources alone decides what the GMC stamps on the Deployment. That is unchanged by this block, and it is the sizing actually in effect whenever the autoscaler is not actuating: mode Off, the VPA components absent or down, or before the first recommendation is produced.
  • The managed VerticalPodAutoscaler is pinned to controlledValues: RequestsOnly, so the autoscaler adjusts requests only. The limits from agcResources (or the platform default) are never raised or lowered by it — the memory limit stays the hard OOM ceiling the operator chose for a single-pod control plane.
  • A request the tenant explicitly set in agcResources becomes the autoscaler's minAllowed for that resource: an explicit floor is honored as a floor rather than silently overwritten. Leave agcResources.requests unset to let the autoscaler size the AGC all the way down to its own global floor — that is the configuration where right-sizing has the most to win.
  • The effective limits become maxAllowed. This is a correctness requirement, not a preference: a container whose request exceeds its own limit is rejected by the apiserver, so the autoscaler must never recommend above the stamped limit.

When the VerticalPodAutoscaler CRD is not installed

The autoscaling.k8s.io CRDs are not part of core Kubernetes. If they are absent the GMC does not fail the gateway: the AGC is provisioned normally with its agcResources sizing, Ready is unaffected, and the advisory AGCAutoscalingUnavailable condition goes True (reason VPACRDNotInstalled) with a matching Warning Event. The gateway is re-probed on a slow bounded requeue, so installing the VPA controllers later converges without an operator edit and without a hot reconcile loop.

Appears in: - ActionsGatewaySpec

Field Description Default Validation
mode VPAUpdateMode Mode is the managed VerticalPodAutoscaler's updateMode. Defaults to Off — recommendation-only, so opting in never restarts the AGC by itself. Set Recreate to let the autoscaler actuate by evicting the AGC pod, or Initial to apply the recommendation only at the next restart. Off Enum: [Off Initial Recreate]
Optional: {}

ActionsGateway

ActionsGateway is a namespace-scoped CRD reconciled by the GMC: it binds a GitHub identity and provisions the per-tenant AGC control plane. v2 permits multiple per namespace (multi-gateway support lands in M3b).

Field Description Default Validation
apiVersion string actions-gateway.com/v2beta1
kind string ActionsGateway
metadata ObjectMeta Refer to Kubernetes API documentation for fields of metadata.
spec ActionsGatewaySpec
status ActionsGatewayStatus

ActionsGatewaySpec

ActionsGatewaySpec is the desired state of a v2 ActionsGateway: the GitHub identity plus the AGC control plane only. v2 decomposes the v1alpha1 monolith — the inline proxy moves to a standalone EgressProxy and the bootstrap runner groups become explicit RunnerSet objects, both removed from this spec (§H.4). Multiple ActionsGateways are permitted per namespace in v2 (the singleton rule is dropped); the multi-gateway controller behavior lands in M3b.

The Pod Security Admission level is NOT a field here. v1alpha1 hung securityProfile on this per-gateway object, but Pod Security Admission is a namespace-scoped control in Kubernetes, so under multi-gateway two gateways in one namespace would fight over the single namespace PSA label. v2 moves the profile to the namespace itself — the operator sets the actions-gateway.com/security-profile label on the tenant namespace and the GMC stamps the PSA labels from it (GMC-guarded, §H.16 #7). See docs/operations/security-operations.md.

Appears in: - ActionsGateway

Field Description Default Validation
credentials GitHubCredentials Credentials configures how this gateway authenticates to GitHub. It is a discriminated union keyed by credentials.type: exactly the member the discriminator names is set (GitHubApp today; workload identity joins as an additive second member, Q197). v2 nests the credential under this explicit-discriminator parent before the v2beta1 freeze so adding an auth method never reshapes the spec again (§H.15).
githubURL string GitHubURL is the GitHub organization, enterprise, or repository URL this gateway's runners register against (e.g. "https://github.com/my-org"). It is immutable: rebinding a running gateway's GitHub org is a footgun, so v2 freezes it via a CEL transition rule (§H.15). Casing follows the v2 convention — "github" is one lowercased word, the trailing initialism stays uppercase. MaxLength: 2048
MinLength: 1
Pattern: ^https://
githubCABundleRef LocalConfigMapReference GitHubCABundleRef names a ConfigMap in this namespace holding, under the key "ca.crt", a PEM certificate bundle to trust when reaching githubURL. Set it when a GitHub Enterprise Server appliance is fronted by a private or internal certificate authority: the AGC and its worker pods otherwise trust the system roots (plus the egress proxy's own CA) and the TLS handshake fails.
The bundle is additive — the system roots stay trusted — so a gateway that also reaches public hosts is unaffected. Certificates are public material, which is why the carrier is a ConfigMap rather than a Secret.
Resolved at runtime, not at admission (§H.7): a ref naming a ConfigMap that is missing, or whose ca.crt holds no parseable certificate, fails the gateway closed (Degraded, reason CABundleNotFound / CABundleInvalid) until it resolves.
Unset (the default) trusts the system roots only. It does not affect egress reachability — a GHES appliance's address space must still be allowed by the egress policy (see the GitHubEgressIncomplete condition).
Optional: {}
defaultProxyRef ProxyObjectRef DefaultProxyRef names an EgressProxy used for AGC control-plane egress and inherited by RunnerSets under this gateway that do not set their own proxyRef. Optional: unset means the control plane egresses directly (subject to NetworkPolicy). Same-namespace unless the target EgressProxy grants cross-namespace use (§H.4, §H.9). Optional: {}
defaultTemplateRef ObjectRef DefaultTemplateRef names a RunnerTemplate (default) or ClusterRunnerTemplate (set kind: ClusterRunnerTemplate) inherited as the worker pod shape by RunnerSets under this gateway that set no spec.templateRef of their own (Q172). Optional: it is the second rung of the template-resolution chain — an unset RunnerSet templateRef resolves rs.templateRef → this defaultTemplateRef → the single cluster-default ClusterRunnerTemplate → fail-closed TemplateNotFound (§H.4). Resolved at runtime in the gateway's own namespace (for a RunnerTemplate); a ClusterRunnerTemplate referent is cluster-scoped. A defaultTemplateRef that names a missing template fails the inheriting set closed (TemplateNotFound), exactly like an explicit templateRef. Optional: {}
agcResources ResourceRequirements AGCResources tunes the CPU/memory requests and limits stamped on this gateway's AGC control-plane container (Q171). It is an additive, per-key override of the platform default — the documented Appendix A sizing of requests {cpu: 500m, memory: 2Gi}, limits {cpu: 2, memory: 4Gi}. The GMC starts from that default and overlays only the request/limit keys set here, so a value that sets just one knob keeps the sensible default for the others. Unset ⇒ the platform default unchanged. Changing it is a rolling restart of the AGC, not a hot reload.
Tune cautiously: the AGC is a single pod holding all listener-goroutine state in memory. A memory limit below the AGC's working set OOMKills the control plane, and a request larger than any node (or the namespace ResourceQuota) leaves the AGC pod unschedulable (Pending). See docs/operations/tenant-onboarding.md and docs/design/appendix-e-capacity-planning.md for sizing guidance and the recommended floor.
Optional: {}
agcAutoscaling AGCVerticalAutoscaling AGCAutoscaling opts this gateway's AGC control-plane pod into managed vertical right-sizing (Q360): the GMC stamps a VerticalPodAutoscaler next to the AGC Deployment so the autoscaler observes actual usage and sizes the container's resource requests for it. Unset (the default) ⇒ no VerticalPodAutoscaler is created and any one the GMC previously stamped is deleted, so the opt-in is off by default and fully reversible.
It composes with — it does not replace — agcResources. See AGCVerticalAutoscaling for the precedence rules and the behavior when the VerticalPodAutoscaler CRD is not installed in the cluster.
Optional: {}
logLevel string LogLevel controls the log verbosity of this tenant's AGC. Allowed values: info (default), debug. Changing it is a rolling restart of the AGC, not a hot reload. Use debug only for a bug repro. info Enum: [info debug]
Optional: {}
tracing TracingConfig Tracing configures opt-in OpenTelemetry distributed tracing for this tenant's AGC. Tracing stays off unless tracing.endpoint is set. Optional: {}
scheduling PodScheduling Scheduling places this gateway's AGC control-plane pod on specific nodes — for example onto the same tenant node pool as the proxy pool, or onto a control-plane pool distinct from where workers run (Q282). Unlike the EgressProxy, the AGC pod carries no built-in affinity, so the block applies verbatim. See PodScheduling. Optional: {}
clusterCapacity ClusterCapacity ClusterCapacity states facts about the cluster this gateway runs in that the AGC cannot safely determine for itself (Q470). It is platform-operator knowledge, not tenant knowledge, which is why it lives here rather than on the RunnerSets that consume it. Optional: {}

ActionsGatewayStatus

ActionsGatewayStatus is the observed state of an ActionsGateway, following the uniform v2 status/condition contract (§H.7).

Appears in: - ActionsGateway

Field Description Default Validation
conditions Condition array Conditions are the observed conditions of the gateway. Known types: Ready, AGCAvailable, CredentialUnavailable, Degraded, EgressUnattributed, RunnerSetsDegraded. Optional: {}
proxyMode string ProxyMode records how this gateway's AGC control-plane egress reaches GitHub: "Proxied" (through the EgressProxy named by defaultProxyRef, with stable per-tenant egress IPs) or "Direct" (no defaultProxyRef, still NetworkPolicy- restricted to GitHub + DNS + kube API but without per-tenant IP attribution). Explicit so "no proxy" is an auditable state, not an inferred absence (§H.10). Enum: [Proxied Direct]
Optional: {}
observedGeneration integer ObservedGeneration is the .metadata.generation the most recent reconcile acted on. Optional: {}

CapacityGate

CapacityGate configures the placeability rung of the admission ladder (Q405).

Without it, a runner set whose worker shape has become unplaceable — a drained GPU pool, a changed taint, spot capacity gone — keeps claiming jobs, and each claim spends a single-use JIT runner record, holds a GitHub job lock until pendingPodDeadline, and ends in a reaped pod plus a CANCELLED workflow run. The gate does not eliminate the first wasted claim (the signal is derived from a stuck pod, so one has to exist); it bounds the RATE, turning a burst of N wasted claims into roughly one per pendingPodDeadline window.

That derivation is also what makes the gate self-clearing: the reaper deletes the stuck pod at the deadline, the condition clears, one job is claimed, and if capacity is still absent the new pod trips it again.

Fail-open by contract at every step — an unreadable set, an unresolved template chain, an unreadable pod list all leave intake exactly as it is today. The gate may under-gate freely; it must never over-gate, because over-gating starves a tenant.

Appears in: - RunnerSetSpec

Field Description Default Validation
mode string Mode selects how hard this set tries not to claim work it cannot run, by naming how the AGC learns the cluster cannot place a worker; see the CapacityGateMode* constants. Off is the default and is today's behavior. Observe gates on evidence an already-stuck pod produced — it is not a report-only tier.
It does NOT select the signal. Which signal is sound depends on whether the cluster can grow, which is stated once by the platform operator on ActionsGateway.spec.clusterCapacity.nodeAutoscaling — so a tenant enabling the gate cannot pick a signal that is wrong for the cluster they are running in.
Off Enum: [Off Observe]
Optional: {}

ClusterCapacity

ClusterCapacity carries the cluster-level facts a capacity decision depends on (Q470). Today that is one fact — whether a node autoscaler runs — with room for the ProvisioningRequest API's availability to join it when that mode ships (Q407).

The block exists because these are properties of the CLUSTER, identical for every RunnerSet in it, and known to whoever owns the nodes. Asking each RunnerSet to assert them would ask tenants to speak for infrastructure they may not own, and would let two sets in one cluster disagree about a fact that has one answer.

Appears in: - ActionsGatewaySpec

Field Description Default Validation
nodeAutoscaling string NodeAutoscaling states whether anything in this cluster adds nodes in response to unschedulable pods; see the NodeAutoscaling* constants. It selects which signal an opted-in RunnerSet capacity gate may trust, because an unschedulable pod means opposite things depending on the answer: where a node may still arrive, that pod is a REQUEST and only the autoscaler's own declination proves nothing is coming; where none can, the pod is pure waste and the scheduler's verdict is enough. Present Enum: [Present Absent]
Optional: {}

ClusterRunnerTemplate

ClusterRunnerTemplate is the cluster-scoped sibling of RunnerTemplate, with an identical spec. It lets the platform own golden privileged templates (DinD, sysbox) once cluster-wide; a RunnerSet selects it with templateRef.kind: ClusterRunnerTemplate (§H.4). Pure data: nothing owns it and it owns nothing.

Field Description Default Validation
apiVersion string actions-gateway.com/v2beta1
kind string ClusterRunnerTemplate
metadata ObjectMeta Refer to Kubernetes API documentation for fields of metadata.
spec RunnerTemplateSpec
status RunnerTemplateStatus

ContainerSizingRecommendation

ContainerSizingRecommendation is the measured-usage-derived resource recommendation for one container of a runner set's worker pods (Q359 Phase 2). Derivation follows the worker right-sizing model (docs/operations/worker-rightsizing.md): requests from the p95 of per-job usage peaks, a memory limit with OOM headroom above the observed maximum, and deliberately no CPU limit (CPU is compressible — a limit only throttles bursty jobs for no packing benefit).

Appears in: - RunnerSetStatus

Field Description Default Validation
container string Container is the container name in the worker pod template this recommendation applies to.
requests ResourceList Requests are the recommended resource requests (cpu, memory), derived from the p95 of observed per-job usage peaks and rounded up to coarse increments (sizing is bucket-granular, not exact). Optional: {}
limits ResourceList Limits are the recommended resource limits. Memory only, at the observed maximum peak plus OOM headroom; no CPU limit is ever recommended. Optional: {}
observedPeak ResourceList ObservedPeak is the highest per-job usage peak (cpu, memory) observed in the window — the input to the recommended memory limit. Optional: {}
observedP95 ResourceList ObservedP95 is the 95th percentile of per-job usage peaks (cpu, memory), bucket-interpolated — the input to the recommended requests. Optional: {}
sampleCount integer SampleCount is the number of finished jobs whose usage peaks fed this recommendation — the operator's confidence signal. Jobs shorter than one sampling interval are not counted (see the worker usage metrics).
windowStartTime Time WindowStartTime is when this container's observation window began (first sampled job, surviving AGC restarts via the re-seed). Optional: {}

CredentialType

Underlying type: string

CredentialType is the discriminator of the GitHubCredentials union: it names which authentication method a gateway uses. The member matching this value must be set and every other member absent. GitHubApp is the possession model (the App key lives in a namespace Secret); WorkloadIdentity is the delegation model (Q197) — no key in the cluster, an external signer signs the App JWT. WorkloadIdentity joined by extending this enum and adding a union member, a non-breaking addition — the whole reason the discriminated-union shape was fixed before the v2beta1 cut (§H.15).

Validation: - Enum: [GitHubApp WorkloadIdentity]

Appears in: - GitHubCredentials

Field Description
GitHubApp CredentialTypeGitHubApp selects GitHub App authentication (the possession model): the gateway holds the App's RSA private key in a namespace Secret named by GitHubCredentials.GitHubApp.
WorkloadIdentity CredentialTypeWorkloadIdentity selects workload-identity authentication (the delegation model, Q197): no App private key is held in the cluster. The AGC proves its pod identity to an external trust anchor (Vault Kubernetes auth in the MVP) and the anchor signs the App JWT via an external signer. Configured by GitHubCredentials.WorkloadIdentity.

EgressPeer

EgressPeer identifies a single destination the GMC may open in an otherwise default-deny per-tenant NetworkPolicy: a pod/namespace selector for an in-cluster peer, or a CIDR for an external one. Exactly one peer form must be given — a CIDR cannot carry a selector and vice versa — so the rule the GMC emits is always scoped to one peer, never a broaden-to-all-egress.

This is the shared egress-peer descriptor that current and future NetworkPolicy egress holes reference, so the v2 API freezes one consistent shape rather than a per-feature near-duplicate (Q204). Today the Vault signer (VaultSigner.NetworkPolicy) is its only consumer; foreseen future consumers — cloud KMS signers, AGC telemetry endpoints — reuse it additively. See docs/design/appendix-g-future-enhancements.md §G.9.

Appears in: - VaultSigner

Field Description Default Validation
podSelector LabelSelector PodSelector selects the in-cluster peer pods the AGC may reach (e.g. {app.kubernetes.io/name: vault}). Combined with NamespaceSelector when both are set, matching NetworkPolicy peer semantics. Mutually exclusive with CIDR. Optional: {}
namespaceSelector LabelSelector NamespaceSelector selects the namespace(s) the in-cluster peer runs in (e.g. {kubernetes.io/metadata.name: vault}). Combined with PodSelector when both are set. Mutually exclusive with CIDR. Optional: {}
cidr string CIDR is the IP block of an external (out-of-cluster) peer, e.g. "10.0.5.7/32". Mutually exclusive with the pod/namespace selectors. MaxLength: 43
Pattern: ^([0-9]\{1,3\}\.)\{3\}[0-9]\{1,3\}/[0-9]\{1,2\}$\|^([0-9a-fA-F:]+)/[0-9]\{1,3\}$
Optional: {}
port integer Port optionally pins the destination port (1–65535) the egress rule permits. Leave it unset for a peer whose port is derivable elsewhere — the Vault signer derives it from VaultSigner.Address, so an unset Port preserves that behavior. Set it for peers whose port is not otherwise derivable, or to override the derived port. Maximum: 65535
Minimum: 1
Optional: {}

EgressPolicyMode

Underlying type: string

EgressPolicyMode selects how the GMC expresses the proxy pool's GitHub egress allowlist. It is TENANT INTENT: the tenant says how egress should be expressed (by CIDR or by hostname); when hostname (FQDN) intent is chosen the platform operator — not the tenant — selects the enforcement mechanism via the GMC --fqdn-policy-backend flag (cilium, calico, or gke). This split keeps the tenant API stable as CNI/platform FQDN mechanisms proliferate (Q245).

  • CIDR (the default) emits a standard Kubernetes NetworkPolicy whose egress allowlist is the GitHub IP ranges, refreshed from api.github.com/meta every 24h by the GMC's IPRangeReconciler. It works on every NetworkPolicy-enforcing CNI and requires no DNS awareness or operator backend.
  • FQDN expresses the GitHub (and any Q242 destinationFQDNs) allowlist by hostname. The GMC emits a CNI-native, DNS-aware egress policy whose kind is chosen by the operator's --fqdn-policy-backend. FQDN intent is REJECTED at admission when the cluster declares no backend (--fqdn-policy-backend=none, the default) — fail- closed and loud, never a silent runtime Degraded.
  • CiliumFQDN / CalicoFQDN are DEPRECATED aliases retained for backward compatibility. Each pins its namesake mechanism (a CiliumNetworkPolicy with toFQDNs, or a Calico NetworkPolicy with destination domains) regardless of the operator backend. Prefer FQDN + --fqdn-policy-backend; these values still work, and the earliest release that may remove them is v3.0.0 — NOT the v2.0.0 that removes v1alpha1, v2alpha1, and classic acquisition. They are enum members of the served beta version v2beta1, which v2.0.0 keeps serving; an API element is removable only by incrementing the version, so the aliases live exactly as long as v2beta1 does, and v3.0.0 is the earliest major tag that can retire it. See docs/operations/v1alpha1-deprecation.md.

All FQDN-family modes are fail-closed: the standard NetworkPolicy still default-denies GitHub egress (it drops the GitHub-CIDR rule but keeps a DNS-only allow), so if the CNI cannot enforce the native policy — or the additive-allow GKE FQDNNetworkPolicy is absent — GitHub egress stays denied rather than opening wide. Selecting an FQDN mode therefore never silently weakens the default.

Validation: - Enum: [CIDR FQDN CiliumFQDN CalicoFQDN]

Appears in: - EgressProxySpec

Field Description
CIDR EgressPolicyModeCIDR is the default: a standard NetworkPolicy with the GitHub IP-range allowlist, refreshed every 24h. Works on every CNI.
FQDN EgressPolicyModeFQDN expresses the GitHub egress allowlist by hostname. The concrete CNI-native policy kind is chosen by the operator's --fqdn-policy-backend (cilium/calico/gke); FQDN intent with no backend is rejected at admission.
CiliumFQDN EgressPolicyModeCiliumFQDN is a DEPRECATED alias for FQDN that pins the Cilium backend (a CiliumNetworkPolicy with toFQDNs) regardless of --fqdn-policy-backend. Prefer FQDN + --fqdn-policy-backend=cilium. Removable no earlier than v3.0.0; see the EgressPolicyMode doc comment for why v2.0.0 cannot remove it.
CalicoFQDN EgressPolicyModeCalicoFQDN is a DEPRECATED alias for FQDN that pins the Calico backend (a projectcalico.org/v3 NetworkPolicy with destination domains) regardless of --fqdn-policy-backend. Prefer FQDN + --fqdn-policy-backend=calico. Removable no earlier than v3.0.0; see the EgressPolicyMode doc comment for why v2.0.0 cannot remove it.

EgressProxy

EgressProxy is a namespace-scoped CRD reconciled by the GMC into a shared egress proxy pool. It is referenced by RunnerSets (proxyRef) and ActionsGateways (defaultProxyRef) by name; referrers never own it (§H.8).

Field Description Default Validation
apiVersion string actions-gateway.com/v2beta1
kind string EgressProxy
metadata ObjectMeta Refer to Kubernetes API documentation for fields of metadata.
spec EgressProxySpec
status EgressProxyStatus

EgressProxySpec

EgressProxySpec is the desired state of a standalone, optionally shared egress proxy pool — v1alpha1's inline ActionsGateway.spec.proxy promoted to its own kind so any number of RunnerSets can point at one pool (§H.4, §H.5). Reconciled by the GMC, which owns the proxy Deployment/Service/HPA/PDB (the reconciler lands in M2).

Appears in: - EgressProxy

Field Description Default Validation
minReplicas integer MinReplicas is the floor of the proxy pool's HPA. 2 Minimum: 1
Optional: {}
maxReplicas integer MaxReplicas is the ceiling of the proxy pool's HPA. 10 Maximum: 100
Minimum: 1
Optional: {}
targetCPUUtilizationPercentage integer TargetCPUUtilizationPercentage is the proxy HPA's target CPU utilization. This is the managed-default knob; set managedAutoscaling: false to bring your own autoscaler instead. 60 Optional: {}
managedAutoscaling boolean ManagedAutoscaling controls whether the GMC manages this proxy pool's HorizontalPodAutoscaler (Q173). Defaults to true: the GMC provisions an HPA scaling the pool on CPU between minReplicas and maxReplicas. Set to false to bring your own autoscaler — the GMC then creates only the proxy Deployment (stable "-proxy" name and labels, replicas left to the external scaler) and deletes any HPA it previously managed, so KEDA, VPA, or a custom HPA can target the Deployment without fighting the managed one. While false: maxReplicas and targetCPUUtilizationPercentage are inert; minReplicas seeds only the Deployment's initial replica count; the Ready condition compares ready pods against the Deployment's own desired replicas (an intentional external scale-to-zero is Ready, not a wedge); and the managed "-proxy" HPA name stays reserved. Mirrors the managedNetworkPolicy opt-out pattern; it shifts autoscaling ownership only and relaxes no security property. true Optional: {}
resources ResourceRequirements Resources are the proxy container's resource requirements. Optional: {}
logLevel string LogLevel controls the log verbosity of this proxy pool. Allowed values: info (default), debug. The GMC threads it to the proxy container as the LOG_LEVEL environment variable; changing it is a rolling restart of the proxy pool (not a hot reload), so the new level takes effect once the pods roll. Use debug only for a bug repro — per-CONNECT debug lines dominate log volume under load. The default is info so a pool never silently runs at debug verbosity. info Enum: [info debug]
Optional: {}
noProxyCIDRs string array NoProxyCIDRs lists destinations excluded from the per-tenant egress proxy (appended to NO_PROXY). Entries may be CIDR prefixes, bare IPs, or NO_PROXY domain suffixes for internal destinations. Never list GitHub here — an entry that routes GitHub traffic around the proxy defeats per-tenant egress-IP attribution and is rejected by the GMC admission path. The admission check covers the public GitHub hosts plus the gitHubURL host — a GitHub Enterprise Server host included — of every ActionsGateway or RunnerSet that references this proxy, on both the proxy write and the referrer write.
The cluster-internal destinations are appended automatically and need not be listed: svc.cluster.local, localhost and 127.0.0.1 for both the AGC and its workers, plus kubernetes.default.svc and this cluster's API server ClusterIP (read from KUBERNETES_SERVICE_HOST) for the AGC, which dials the API server by IP. Every entry added here bypasses the proxy and so escapes the per-tenant egress-IP attribution: exempt specific internal destinations, never a broad range.
Optional: {}
managedNetworkPolicy boolean ManagedNetworkPolicy controls whether the GMC manages this proxy's egress NetworkPolicy. Defaults to true (secure default). true Optional: {}
egressPolicyMode EgressPolicyMode EgressPolicyMode selects how the GMC expresses the proxy pool's GitHub egress allowlist: the default CIDR mode (standard NetworkPolicy + 24h IP-range reconcile, works on every CNI) or an FQDN intent (a CNI-native DNS-aware policy whose mechanism the operator picks via --fqdn-policy-backend). The deprecated CiliumFQDN/CalicoFQDN aliases pin their namesake backend and stay accepted until v3.0.0 at the earliest. It has no effect when managedNetworkPolicy is false. See the EgressPolicyMode docs for the secure-by-default (fail-closed) guarantee. CIDR Enum: [CIDR FQDN CiliumFQDN CalicoFQDN]
Optional: {}
destinationFQDNs string array DestinationFQDNs lists EXTRA, non-GitHub DNS host suffixes the proxy may forward worker CONNECT traffic to (e.g. proxy.golang.org). GitHub is always allowed and need not be listed; empty (the default) means GitHub-only. Host-suffix entries REQUIRE an FQDN egressPolicyMode (FQDN, or the deprecated CiliumFQDN/CalicoFQDN), since the pod-egress layer expresses them as toFQDNs rules. Opening egress beyond GitHub is an admin decision: the GMC rejects any entry not on its --allowed-egress-fqdns platform allowlist (empty allowlist denies all). G.1 / Q242; see design Appendix G.1 (Proxy-Enforced Destination Allowlist). MaxItems: 64
Optional: {}
destinationCIDRs string array DestinationCIDRs lists EXTRA, non-GitHub IP ranges the proxy may forward to (e.g. an internal 10.x subnet with no DNS, or a cloud private-API range). CIDR entries work in ANY egressPolicyMode — they become ipBlock egress peers (CIDR mode) or toCIDR peers (FQDN mode). The GMC rejects any entry not contained in its --allowed-egress-cidrs platform allowlist (empty denies all). MaxItems: 64
Optional: {}
sharing ProxySharing Sharing controls cross-namespace reference to this proxy. nil means same-namespace only (the default, secure). Consent lives on the provider (proxy owner) side: a consumer-side name alone never authorizes the reference (§H.9). v2 ships the inline allowlist only. Optional: {}
scheduling PodScheduling Scheduling pins this proxy pool's pods to specific nodes — the mechanism that binds a tenant to a per-tenant egress IP (Q243/Q282), since the pod's node determines which cloud NAT / egress path its traffic leaves by. A podAntiAffinity set here replaces the built-in required cross-node spread; see PodScheduling for the full precedence rules and the tenant-settable-by-design rationale. Optional: {}

EgressProxyStatus

EgressProxyStatus is the observed state of an EgressProxy, following the uniform v2 status/condition contract (§H.7). Nothing owns an EgressProxy and it owns its own children; deletion degrades referrers rather than blocking, so it carries no finalizer (§H.8).

Appears in: - EgressProxy

Field Description Default Validation
conditions Condition array Conditions are the observed conditions of the proxy pool. Known types: Ready, Degraded, ProxyQuotaPressure, ProxyQuotaExceeded, EgressRulesStale (Q320). Optional: {}
readyReplicas integer ReadyReplicas is the number of ready proxy pods. Optional: {}
observedGeneration integer ObservedGeneration is the .metadata.generation the most recent reconcile acted on. Optional: {}

ExternalSigner

ExternalSigner is the discriminated union of external signing backends. Provider is the explicit discriminator: exactly the member it names is set. The union exists so cloud KMS providers add as members without reshaping the spec — the same additive contract as the GitHubCredentials union above. The "exactly the named member is set" invariant is enforced by a per-provider CEL iff rule that each new provider extends.

Appears in: - WorkloadIdentity

Field Description Default Validation
provider SignerProvider Provider selects the active signer backend (the union discriminator). The member it names must be set and all others absent. Enum: [Vault]
Required: {}
vault VaultSigner Vault configures the HashiCorp Vault transit signer. Set iff Provider is Vault. Optional: {}

GitHubCredentials

GitHubCredentials is the discriminated union of the ways a gateway authenticates to GitHub. Type is the explicit discriminator (k8s union convention): exactly the member it names is set. Today the only member is GitHubApp; workload identity joins as a second member without a breaking change (Q197) — the union shape exists so adding an auth method never reshapes the spec again after the v2beta1 freeze (§H.15). The "exactly the named member is set" invariant is enforced by CEL (the apiserver does not enforce native union semantics on CRDs): one per-member iff rule that each new member extends, never an N-way "exactly one of" that grows with the union.

Appears in: - ActionsGatewaySpec

Field Description Default Validation
type CredentialType Type selects the active credential member (the union discriminator). The member it names must be set and all others absent. It is required and explicit — an absent or implicit discriminator would have to become required later, itself a breaking change, so v2 fixes it before beta. Enum: [GitHubApp WorkloadIdentity]
Required: {}
githubApp LocalSecretReference GitHubApp configures GitHub App authentication: a name-only reference to the Secret in this gateway's own namespace holding the App credentials ({appId, installationId, privateKey}). Set iff Type is GitHubApp. The reference name is deliberately mutable — changing it is the supported credential-rotation path. Optional: {}
workloadIdentity WorkloadIdentity WorkloadIdentity configures workload-identity authentication (Q197): the App JWT is signed by an external signer the AGC reaches by proving its pod identity, so no App private key is ever held in the cluster (the delegation model). Set iff Type is WorkloadIdentity. This is the on-strategy, no-PEM credential method; GitHubApp remains the secure-by-default in-cluster option. Optional: {}

LocalConfigMapReference

LocalConfigMapReference is a name-only reference to a Kubernetes ConfigMap in the same namespace. It is distinct from LocalObjectRef because the referent is a core object, not a v2 one: its name budget is the full 253-character DNS subdomain rather than the 52-character v2 object budget (§H.6).

Appears in: - ActionsGatewaySpec

Field Description Default Validation
name string Name of the referenced ConfigMap in the same namespace. MaxLength: 253
MinLength: 1

LocalSecretReference

LocalSecretReference is a name-only reference to a Kubernetes Secret in the same namespace. v2 drops v1alpha1's SecretReference.namespace field: it was reserved-but-validated-empty and read like a cross-namespace reference that does not exist — a confused-deputy footgun. The referenced Secret must reside in the referrer's own namespace (§H.15).

Appears in: - GitHubCredentials

Field Description Default Validation
name string Name of the referenced Secret in the same namespace. MaxLength: 253
MinLength: 1

NodeShareSizing

NodeShareSizing declares the per-node allocatable envelope the NodeShare profile divides among workers. The operator declares the envelope rather than the AGC reading Node objects: the AGC is deliberately namespace-scoped (no cluster-scoped RBAC), and the operator knows which node shape the set's scheduling constraints target.

Appears in: - WorkerSizing

Field Description Default Validation
allocatable ResourceList Allocatable is the node's allocatable cpu/memory to divide (from kubectl describe node, minus any system/sidecar overhead the operator reserves). Keys other than cpu and memory are ignored, so at least one of the two must be present: an envelope carrying neither (empty, or GPUs only) derives nothing while the profile still reports Active (Q484). Declaring just one is legitimate — the other resource keeps the template's ask.
workersPerNode integer WorkersPerNode is the divisor: the number of worker pods that should pack onto one node (for GPU nodes, typically the GPU count). Maximum: 1000
Minimum: 1

ObjectRef

ObjectRef is a name-only reference to another v2 object in the same namespace.

It backs gatewayRef, templateRef and defaultTemplateRef, whose referents are always resolved in the referrer's own namespace. Cross-namespace use of those three is deliberately not expressible, since no consent handshake stands behind them; the one referent kind that does grant cross-namespace use, EgressProxy, has its own ProxyObjectRef (§H.9). Referential integrity is a runtime condition, not an admission gate (§H.7), so a ref naming a not-yet-applied object is well-formed, and the controller surfaces a NotFound condition until it resolves.

Appears in: - ActionsGatewaySpec - RunnerSetSpec

Field Description Default Validation
name string Name of the referenced object. Bounded by the same 52-char budget every v2 object name carries (§H.6), so a well-formed ref can always name a valid object. MaxLength: 52
MinLength: 1
kind string Kind optionally selects the referent kind when more than one is valid. It is load-bearing only on templateRef, where it chooses between RunnerTemplate (the namespaced default) and ClusterRunnerTemplate (a platform golden template). gatewayRef and proxyRef each have a single valid kind, so Kind is left empty there. Enum: [RunnerTemplate ClusterRunnerTemplate]
Optional: {}

PodScheduling

PodScheduling is the pod placement pass-through shared by EgressProxy (the proxy pool pods) and ActionsGateway (the AGC control-plane pod). It carries the three standard Kubernetes placement controls verbatim, so the pods GAG builds on a tenant's behalf can be steered exactly like any other pod.

Why it exists (Q243/Q282). A per-tenant egress IP is realized by pinning a tenant's proxy pods to a node pool whose egress path (cloud NAT gateway, dedicated subnet, or SNAT-ing gateway node) owns that IP. Without a placement knob on the EgressProxy, the proxy pool's replicas spread across whatever pools exist and one tenant egresses from several IPs — the gap the Q243 live validation found. Worker pods already have the full placement surface via RunnerTemplate.podTemplate; this type closes the same gap for the two pods GAG owns directly.

Placement is tenant-settable by design

These fields are NOT gated by a platform allowlist, and that is a deliberate, reviewed decision rather than an oversight (Q282):

  • Choosing an egress path is a feature, not an escape. Distinct per-tenant egress IPs exist so tenants do not share a rate-limit or block radius with each other. A tenant electing where its own traffic leaves the cluster is the intended use.

  • The capability is already reachable, though the symmetry is partial — state it precisely. RunnerTemplate.podTemplate is a full PodTemplateSpec whose reserved-field guardrail (§H.4) withholds serviceAccountName, automountServiceAccountToken, hostPID, hostNetwork, and hostIPC — but deliberately not nodeSelector/tolerations/affinity. In DIRECT-egress mode (§H.10, no proxyRef) worker pods reach GitHub without a proxy, so worker placement already selects the egress IP with no gate. In PROXIED mode (the default) it does not: NetworkPolicy forces worker traffic through the proxy, so the proxy pool's placement is what selects the IP. This field therefore does extend the capability to the default posture — it is not purely a symmetry argument, and gating it would still leave the direct-egress path open.

  • Constraining placement is the platform's job, and only the platform has a sound tool. A per-CRD validating allowlist of permitted placement values cannot work: affinity.nodeAffinity supports NotIn/DoesNotExist, so "any pool except my own" is expressible and no key=value allowlist rejects it in general. Pinning nodeSelector by MUTATION is sound — Kubernetes ANDs nodeSelector with nodeAffinity, so affinity can only narrow the candidate nodes, never widen them — and mutation is a policy-engine (Gatekeeper, Kyverno) capability, not a CRD webhook one. See docs/operations/admission-policies.md for ready-to-apply samples covering this type and RunnerTemplate alike.

The property this weakens is attribution, not isolation: if a tenant retargets its proxy onto another pool's egress path, traffic from both tenants leaves via one IP and per-tenant IP attribution no longer holds. Nothing about namespace isolation, RBAC, or the egress choke point changes. See docs/design/05-security.md.

Why a narrow block rather than a PodTemplateSpec

Worker pods take placement through the full corev1.PodTemplateSpec on RunnerTemplate. This type deliberately does not, for two reasons:

  • Size. A PodTemplateSpec generates ~600 KB of OpenAPI. It is why the RunnerTemplate CRDs weigh 1.21 MB each and why the v2 CRDs ship in their own opt-in chart at all (§H.13, Q149 — embedding them pushed the main chart's Helm release Secret past 1 MiB). Two more copies, across two served versions, would put single CRD objects at the apiserver's ~1.5 MiB ceiling.
  • Ownership. The proxy and AGC pods' image, container, and securityContext are controller-enforced invariants. A PodTemplateSpec would invite an author to set them and then require a reserved-field CEL guardrail to reject it, which is the complexity RunnerTemplate carries and these kinds do not need.

The consequence is a real gap: workers can express topologySpreadConstraints, priorityClassName, schedulerName, runtimeClassName, and nodeName; these pods cannot. The two that matter are topologySpreadConstraints (the modern successor to the anti-affinity spread this proxy pool already relies on) and priorityClassName (an evicted proxy pod takes that tenant's whole egress path down). Growing this struct is purely additive — no conversion work, no breaking change. Tracked as Q284.

Appears in: - ActionsGatewaySpec - EgressProxySpec

Field Description Default Validation
nodeSelector object (keys:string, values:string) NodeSelector constrains the pods to nodes carrying all of these labels — the simplest way to pin a proxy pool to a tenant's node pool (and thus to that pool's egress IP). Merged into the pod spec verbatim. Optional: {}
tolerations Toleration array Tolerations let the pods schedule onto nodes carrying matching taints — the companion to NodeSelector when a tenant's node pool is taint-guarded so that unrelated workloads do not land on it. Applied to the pod spec verbatim. Optional: {}
affinity Affinity Affinity is the standard pod/node affinity and anti-affinity block.
Precedence against the built-in anti-affinity (EgressProxy only). The GMC stamps a REQUIRED cross-node podAntiAffinity on every proxy pool so one node failure cannot take the whole pool down. Composition with this field is:
- nodeAffinity and podAffinity are applied as given, alongside the built-in anti-affinity, which is preserved.
- podAntiAffinity, when set to any non-nil value, REPLACES the built-in term entirely — set it and you own it. An explicit empty value (podAntiAffinity: {}) therefore opts out of cross-node spreading, which is what a single-node tenant pool needs: the required built-in term would otherwise strand every replica after the first in Pending. Lowering minReplicas to 1 is the other way to get there.
ActionsGateway has no built-in affinity, so the block applies verbatim there.
Optional: {}
topologySpreadConstraints TopologySpreadConstraint array TopologySpreadConstraints spreads the pods across failure domains (zones, nodes) — the modern successor to the cross-node podAntiAffinity this proxy pool already relies on. It expresses "spread across zones, tolerate a skew of 1", which anti-affinity cannot. Applied to the pod spec verbatim.
Composition with the built-in anti-affinity (EgressProxy only). Unlike Affinity, this field COMPOSES with the built-in required cross-node anti-affinity — it never replaces it. podAntiAffinity: \{\} on Affinity stays the single opt-out for the built-in cross-node spread; this field does not add a second one. That asymmetry is deliberate: Affinity has to be able to displace the built-in term because podAntiAffinity occupies the same field with nowhere else to go, whereas topologySpreadConstraints is a different field with no such collision, so composing is safe (it can only NARROW the candidate node set, like nodeSelector AND-ing with nodeAffinity) and having one author silently lose cross-node spread by asking for zonal spread would be a footgun. Its labelSelector counts only pods in the constraint's own namespace, so one tenant's spread cannot be skewed by another tenant's pods.
The Pending trap. An author who asks for a SOFT zonal spread (whenUnsatisfiable: ScheduleAnyway) still inherits the REQUIRED built-in cross-node anti-affinity, so replicas beyond the pinned pool's node count strand in Pending — the same behavior as any proxy pool whose minReplicas exceeds its node count. The escapes are the same: podAntiAffinity: \{\} to opt out of the built-in cross-node spread, or lower minReplicas.
Unlike PriorityClassName, this needs no allowlist: it is namespace-scoped and can only narrow placement, so it carries no cross-tenant lever.
Optional: {}
priorityClassName string PriorityClassName names a cluster-scoped PriorityClass for these pods, raising them above best-effort workloads under node pressure. It matters because an evicted proxy pod takes that tenant's ENTIRE egress path down, and an evicted AGC pod takes that tenant's control plane down; without a priority class both are as evictable as any best-effort pod.
Gated by a SEPARATE, infra-only allowlist. This field is validated against the GMC --allowed-infra-priority-classes flag, which is DISJOINT by construction from the worker-facing --allowed-priority-classes that gates priorityTiers and RunnerTemplate podTemplate.spec.priorityClassName. The two must not intersect, and the GMC refuses to start if they do.
Why a second allowlist rather than reusing the worker one. Infra pods must sit ABOVE workers — that is the whole point of prioritizing them. If the worker allowlist were reused and a high class added so a proxy could name it, that same class would become nameable from a worker pod, and any tenant could lift its WORKERS to infra priority and preempt OTHER tenants' proxy pods. The gate meant to protect the proxy would become the mechanism for evicting it.
Why gated at all, when nodeSelector/tolerations/affinity are not. Placement is a choice about the tenant's own traffic; it weakens attribution, not isolation. Priority is a cluster-wide, cross-tenant preemption lever: a pod naming a preempting class can evict OTHER tenants' pods off a node. And system-* PriorityClasses are NOT kube-system-scoped — a pod in any namespace may name system-cluster-critical (value 2000000000, PreemptLowerPriority), with no built-in admission check restricting it (verified against a real apiserver). So an ungated priorityClassName on a tenant-writable CR is a cluster-wide preemption escape, which is why it is the one PodScheduling field behind a gate.
The empty string (the default) names no class and is always permitted, so an unset allowlist forbids every NAMED class without forbidding unprioritized pods.
Optional: {}

PriorityClassAllowlist

PriorityClassAllowlist is the cluster-scoped, platform-owned allowlist of PriorityClass names tenants may reference. It is pure configuration: nothing owns it and it owns nothing.

It exists as a CRD rather than a ConfigMap because it is the paramKind of the priorityclass-allowlist-guard ValidatingAdmissionPolicy, and a kube-apiserver defect (Q444) permanently kills the param informer for any CORE-type paramKind once the set of bindings naming it goes empty for one refresh tick — which a helm uninstall does. The apiserver allocates a fresh dynamic informer per context for a CRD paramKind, so this kind is structurally immune; the contrast is measured by scripts/e2e/vap-param-informer-check.sh. Background: docs/design/05-security.md (PriorityClass allowlist).

Whose fact is it (api-review.md § Ask whose fact it is): the platform's, in every sense — only whoever runs the nodes knows which classes exist and what preempting one costs, and a cap a tenant could raise is not a cap. Hence cluster-scoped, and writable only by a platform admin.

Field Description Default Validation
apiVersion string actions-gateway.com/v2beta1
kind string PriorityClassAllowlist
metadata ObjectMeta Refer to Kubernetes API documentation for fields of metadata.
spec PriorityClassAllowlistSpec

PriorityClassAllowlistSpec

PriorityClassAllowlistSpec is the platform allowlist of PriorityClass names a tenant may reference. It carries two disjoint sets — one for worker pods, one for infra pods — because a class nameable from both surfaces would let a tenant lift its workers to infra priority and preempt other tenants' proxy pods.

Appears in: - PriorityClassAllowlist

Field Description Default Validation
allowedPriorityClasses string array AllowedPriorityClasses is the set of cluster-scoped PriorityClass names tenants may reference from RunnerSet.spec.priorityTiers[].priorityClassName and RunnerTemplate.spec.podTemplate.spec.priorityClassName (and, while v1alpha1 is served, RunnerGroup.spec.priorityTiers[].priorityClassName).
A PriorityClass sets cluster-wide scheduling preemption order, so a tenant naming a high-priority class could evict another tenant's worker pods. The platform admin pre-creates each class (with preemptionPolicy: Never unless a tier is genuinely meant to preempt across tenants) and lists its name here.
Unset or empty forbids every named class — the secure default. This list is additive to the GMC's static --allowed-priority-classes flag for the admission webhook, and is the SOLE source for the priorityclass-allowlist-guard ValidatingAdmissionPolicy, which cannot read a controller flag. Keep it a superset of the flag, or direct writes naming a flag-only class are denied by the policy.
MaxItems: 128
items:MaxLength: 253
items:Pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
Optional: {}
allowedInfraPriorityClasses string array AllowedInfraPriorityClasses is the set of cluster-scoped PriorityClass names a tenant may reference from EgressProxy.spec.scheduling.priorityClassName and ActionsGateway.spec.scheduling.priorityClassName — the INFRA pods, meaning the per-tenant egress proxy pool and the AGC control plane.
Infra pods are meant to outrank workers: an evicted proxy takes a tenant's whole egress path down. That is why this is a SEPARATE set, and why it must stay disjoint from AllowedPriorityClasses — a class on both is nameable from a worker pod, so any tenant could lift its workers to infra priority and preempt another tenant's proxy. The spec-level CEL rule rejects an overlap within this object; the GMC additionally refuses an overlap against its static flags.
Unset or empty forbids every named class — the secure default. This list is additive to the GMC's static --allowed-infra-priority-classes flag. The priorityclass-allowlist-guard ValidatingAdmissionPolicy does not read it: the infra surfaces are gated by failurePolicy=fail webhooks, not by that policy.
MaxItems: 128
items:MaxLength: 253
items:Pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
Optional: {}

PriorityTier

PriorityTier maps a Kubernetes PriorityClass to a cumulative pod-count threshold. Thresholds must be in strictly ascending order. Unchanged in meaning from v1alpha1; carried onto the RunnerSet (was RunnerGroup).

Appears in: - RunnerSetSpec

Field Description Default Validation
priorityClassName string PriorityClassName is the name of an existing cluster-scoped PriorityClass. The platform owns which classes a tenant may reference: the GMC admission path rejects any name not on the platform allowlist (--allowed-priority-classes), so a tenant cannot name a high-priority, preempting class and evict other tenants' worker pods. The named class — including its preemptionPolicy — is platform-created; see docs/operations/security-operations.md. MaxLength: 253
MinLength: 1
threshold integer Threshold is the cumulative active-pod count at which this tier is exhausted. Minimum: 1

ProxyObjectRef

ProxyObjectRef references an EgressProxy, optionally in another namespace. It is deliberately not ObjectRef: that type also backs gatewayRef, templateRef and defaultTemplateRef, and a Namespace field on it would make all four cross-namespace at once, three of them with no consent handshake behind them (§H.9).

Appears in: - ActionsGatewaySpec - RunnerSetSpec

Field Description Default Validation
name string Name of the referenced EgressProxy. Bounded by the v2 52-char name budget (§H.6). MaxLength: 52
MinLength: 1
namespace string Namespace optionally selects an EgressProxy outside the referrer's own namespace. Empty means the referrer's namespace.
A cross-namespace reference resolves only with provider consent: the named proxy must list the referrer's namespace in its spec.sharing.allowedNamespaces. Without that the reference fails closed with ProxyShareNotGranted, and no NetworkPolicy or CA trust is wired. Naming a proxy never authorizes reaching it.
A shared proxy is a shared egress identity, so it suits cooperating tenants or a platform-operated central pool rather than mutually-distrusting tenants.
MaxLength: 63
Optional: {}

ProxySharing

ProxySharing controls cross-namespace reference to an EgressProxy. Consent is always provider-side: the proxy owner publishes which namespaces may reference it (§H.9). A nil ProxySharing means same-namespace only (the default, secure). v2 ships the inline allowlist only; ReferenceGrant support is additive later.

Appears in: - EgressProxySpec

Field Description Default Validation
allowedNamespaces string array AllowedNamespaces lists namespaces permitted to reference this proxy. A consumer-side name alone never authorizes a cross-namespace reference. Optional: {}

RunnerSet

RunnerSet is a namespace-scoped CRD reconciled by the AGC. It binds a worker pod shape (templateRef) and an optional egress proxy (proxyRef) to a GitHub gateway (gatewayRef), and carries the scheduling/quota knobs that were RunnerGroup's in v1alpha1. Worker pods are provisioned per acquired job and released on completion.

The .spec.gatewayRef.name selectable field (KEP-4358, used by the AGC to scope its RunnerSet watch server-side) is deliberately declared ONLY on v2alpha1, NOT here. The apiserver hoists per-version selectableFields to spec level when they are identical across all served versions, but leaves the schema per-version when the versions' schemas differ (v2beta1 drops acquisitionProtocol/maxListeners). A spec-level selectableFields with a nil spec-level schema is rejected ("selectableFields may only be set when validations.schema is included"), so declaring the field on both versions makes the RunnerSet CRD un-installable. The AGC operates on v2alpha1 during coexistence, so v2alpha1 is the version that needs the field selector; when v2beta1 becomes the sole served version (the v2alpha1 removal at the ScaleSet-only cut) the marker moves here and v2alpha1's is dropped — so the two are never declared simultaneously. See Q74.

Field Description Default Validation
apiVersion string actions-gateway.com/v2beta1
kind string RunnerSet
metadata ObjectMeta Refer to Kubernetes API documentation for fields of metadata.
spec RunnerSetSpec
status RunnerSetStatus

RunnerSetSpec

RunnerSetSpec is the desired state of a RunnerSet — the small scheduling/quota binder that replaces v1alpha1's RunnerGroup. The large PodTemplateSpec no longer lives here: it moves to a referenced RunnerTemplate / ClusterRunnerTemplate so a runner set stays a fixed-size object and a template is reused across many sets (docs/design/appendix-h-v2-api-decomposition.md §H.5). The scheduling and lifecycle fields are carried over from RunnerGroup unchanged in meaning.

Appears in: - RunnerSet

Field Description Default Validation
gatewayRef ObjectRef GatewayRef names the ActionsGateway that supplies this runner set's GitHub binding and control plane. Under multi-gateway-per-namespace each AGC reconciles only the RunnerSets whose gatewayRef targets it — which is why spec.gatewayRef.name is a CRD selectable field (KEP-4358), so that scoping runs server-side (§H.7). Required: a runner set with no gateway has no GitHub connection to register against. Resolved at runtime, not admission.
templateRef ObjectRef TemplateRef optionally names the RunnerTemplate (default) or ClusterRunnerTemplate (set kind: ClusterRunnerTemplate) that supplies the worker pod shape. Unset means inherit the gateway's defaultTemplateRef; both unset means the single cluster-default ClusterRunnerTemplate (the one marked IsDefaultTemplateAnnotation). If none of the three resolves the set fails closed Ready=False/TemplateNotFound — the AGC never synthesizes a phantom worker pod without a pod shape (Q172, §H.4). This relaxes the GA-era required templateRef to optional-with-a-default (a backward-compatible required→optional change): a set that sets templateRef behaves exactly as before. The referent is resolved at runtime; a set pointing at a not-yet-applied template sits Ready=False/TemplateNotFound until it syncs (§H.7). status.templateSource reports which rung resolved. Optional: {}
proxyRef ProxyObjectRef ProxyRef optionally names the EgressProxy this runner set's traffic egresses through. Unset means inherit the gateway's defaultProxyRef; both unset means direct egress (still NetworkPolicy-restricted to DNS + GitHub) — a well-defined behavior, so the dependency is simply droppable, which is why proxyRef is optional where templateRef is required (§H.4, §H.10). Direct egress is reflected in status as proxyMode=Direct plus an advisory EgressUnattributed condition; a proxyRef/defaultProxyRef that names a missing proxy still fails closed (ProxyNotFound), not direct egress (Q168). Optional: {}
maxWorkers integer MaxWorkers caps the number of worker pods this RunnerSet may run concurrently. A soft, in-process ceiling; pair it with a namespace ResourceQuota for a hard, cluster-enforced limit. Minimum: 1
Optional: {}
runnerLabels string array RunnerLabels is the label set matched against workflow runs-on values. v2beta1 is ScaleSet-only, so exactly one label is required (enforced by the CEL rule on this field): the scale set's single runnerLabel is its runs-on match target, and the label doubles as the scale-set name registered at GitHub. Each label must be non-empty and contain no whitespace or commas (comma is the runs-on list separator). A workload needing multi-label matching must stay on a v2alpha1 Classic RunnerSet during the deprecation window (Q264 §5a-U7).
The single-label rule lives on THIS FIELD rather than on the spec, so CRD validation ratcheting (KEP-4008; on by default since Kubernetes 1.30, and the v2 floor is 1.31) suppresses it on an update that leaves runnerLabels untouched. A Classic multi-label set authored through v2alpha1 is STORED as a hub object that violates this rule, so a spec-level rule made every unqualified kubectl edit/patch of such a set fail on a field unrelated to labels (Q398). Ratcheting only forgives an unchanged value: creating a multi-label set through v2beta1, or editing a stored set's labels through v2beta1, is still rejected.
MinItems: 1
items:MaxLength: 256
items:Pattern: ^[^,\s]+$
priorityTiers PriorityTier array PriorityTiers defines PriorityClass assignments and cumulative pod-count thresholds. Tiers must be in strictly ascending threshold order. MaxItems: 10
Optional: {}
maxEvictionRetries integer MaxEvictionRetries controls how many times the AGC automatically re-queues a job whose worker pod was evicted. Set to 0 to disable auto-retry entirely. Defaults to 2 when omitted.
Honored on both acquisition tiers as of Q417. On ScaleSet — the only tier this API version offers — the worker is provisioned fire-and-forget, so the eviction is detected by the owning reconciler from the worker pod rather than by a goroutine watching it; the budget itself is shared, keyed by workflow run, so this cap applies per run across both tiers rather than once each.
Maximum: 10
Minimum: 0
Optional: {}
evictionRetryDelay Duration EvictionRetryDelay is the minimum time to wait before re-queuing an evicted job. Must be at least 1s. Defaults to "5s" when omitted.
Honored on both acquisition tiers as of Q417, as for MaxEvictionRetries.
Optional: {}
maxQuotaRetries integer MaxQuotaRetries controls how many times the AGC retries pod creation when the namespace ResourceQuota is exhausted. Set to 0 to disable quota retry. Defaults to 5 when omitted. Maximum: 20
Minimum: 0
Optional: {}
quotaRetryDelay Duration QuotaRetryDelay is the time to wait between pod creation retries when the namespace ResourceQuota is exhausted. Must be at least 1s. Defaults to "30s" when omitted. Optional: {}
completedPodTTL Duration CompletedPodTTL is how long a worker pod that has reached a terminal phase is retained before the AGC deletes it. Set to "0s" to delete immediately on completion. Must not be negative. Defaults to "5m" when omitted. Optional: {}
pendingPodDeadline Duration PendingPodDeadline is the maximum time a worker pod may remain Pending before the AGC deletes it, releasing the concurrency-ceiling slot. Must be at least 1s. Defaults to "10m" when omitted. Optional: {}
maxWorkerLifetime Duration MaxWorkerLifetime is the maximum time a worker pod may be active on its node before the kubelet kills it, applied as the pod's activeDeadlineSeconds. It bounds a worker whose job ended while the AGC was down — a pod nothing the AGC observes later can distinguish from one running a long job (Q438). The kubelet enforces it, so it holds even while the AGC is unavailable.
A pod killed this way lands in Failed with reason DeadlineExceeded and is reaped under reason "lifetime_exceeded", with a Warning Event (WorkerPodLifetimeExceeded). Jobs declaring a timeout-minutes above this will be killed mid-run. Set to "0s" to disable. An activeDeadlineSeconds set explicitly on the RunnerTemplate's podTemplate takes precedence. Must not be negative. Defaults to "12h" when omitted.
Optional: {}
scaleUp ScaleUpRateLimit ScaleUp optionally caps the RATE at which the AGC creates new worker pods for this RunnerSet, smoothing cold-start stampedes on a shared, rate-sensitive egress path (NAT/SNAT gateway, stateful-firewall conntrack table, site-to-site VPN) when a burst of jobs is acquired at once. It is a token bucket over pod CREATION and is distinct from maxWorkers (a ceiling on the concurrent pod COUNT): the ceiling bounds how many run, this bounds how fast they start. Omitted ⇒ no rate limit (the default): immediate provisioning, zero added latency. An availability knob for the narrow stampede case; prefer a peer-to-peer image mirror for image-pull storms and workflow-level concurrency: or the maxWorkers ceiling for fairness/sustained load. Optional: {}
sizing WorkerSizing Sizing opts this runner set into measured worker sizing at pod-build time (Q359 Phase 3): the AGC derives the worker containers' CPU/memory requests/limits from the observed per-job usage history (status.sizingRecommendation) — or, for the NodeShare profile, from a declared per-node share — instead of the template's static values. Omitted (or profile Static) keeps today's behavior: the template is authoritative. Extended resources (GPUs) are never modified by any profile: they are job-selected shape identity, and only the cpu/memory keys are ever derived. Optional: {}
capacityGate CapacityGate CapacityGate opts this runner set into the placeability rung of the admission ladder (Q405): the AGC refuses to take on jobs whose worker pod the cluster cannot currently place, instead of claiming them and stalling. Omitted (or mode Off) keeps today's behavior exactly — no capacity rung. Optional: {}

RunnerSetStatus

RunnerSetStatus is the observed state of a RunnerSet. It follows the uniform v2 status/condition contract (§H.7): a listType=map Conditions slice keyed on type, an ObservedGeneration, and a Ready condition with the shared reason vocabulary (see conditions.go). Reference-resolution failures surface as Ready=False with a specific reason (TemplateNotFound / ProxyNotFound / …) and a message naming the missing object.

Appears in: - RunnerSet

Field Description Default Validation
conditions Condition array Conditions are the observed conditions of the runner set. Known types: Ready, Degraded, EgressUnattributed, PossibleReapBlockingSidecar, WorkerQuotaPressure, WorkerQuotaExceeded, WorkersUnschedulable, RateLimited, RunnerVersionTooOld, SizingDrift. Optional: {}
activeSessions integer ActiveSessions is the number of currently open long-poll sessions. Optional: {}
activeJobs integer ActiveJobs is the number of worker pods currently in the Running phase (a job is actively executing). Derived from the worker pod phase count during each reconcile; see also PendingJobs. Optional: {}
pendingJobs integer PendingJobs is the number of worker pods currently in the Pending phase (a job has been acquired and a pod spawned, but the pod is not yet running — waiting on scheduling, image pull, or node readiness). Pods that remain Pending past spec.pendingPodDeadline are deleted by the controller; a sustained non-zero count warrants checking events and scheduling constraints. Optional: {}
proxyMode string ProxyMode records how this runner set's worker egress reaches GitHub: "Proxied" (through the resolved EgressProxy, with stable per-tenant egress IPs) or "Direct" (no proxyRef/defaultProxyRef, still NetworkPolicy-restricted to GitHub + DNS but without per-tenant IP attribution). Explicit so "no proxy" is an auditable state, not an inferred absence (§H.10). Paired with the advisory EgressUnattributed condition when Direct. Enum: [Proxied Direct]
Optional: {}
templateSource string TemplateSource records which rung of the template-resolution chain supplied this runner set's worker pod shape (Q172, §H.4): "TemplateRef" (its own spec.templateRef), "GatewayDefault" (the gateway's spec.defaultTemplateRef, inherited because templateRef was unset), or "ClusterDefault" (the single cluster-default ClusterRunnerTemplate, resolved because neither was set). Explicit so an operator can audit whether a set runs on an explicit template or a default without inspecting the gateway and cluster state. Empty until the references resolve. Enum: [TemplateRef GatewayDefault ClusterDefault]
Optional: {}
sizingProfileState string SizingProfileState reports whether the opt-in sizing profile (spec.sizing.profile) is actuating: "Active" — derived values are applied at pod build; "AwaitingSamples" — a history-based profile (Binpack / Throughput) is selected but not every template container has a confident recommendation yet, so pods provision with the template's static values until the history accumulates (whole-pod fallback, keeping QoS predictable). Empty when no profile (or Static) is selected. Explicit so "is the profile live yet" is auditable status, not something to infer from pod specs (the proxyMode precedent). Enum: [Active AwaitingSamples]
Optional: {}
sizingRecommendation ContainerSizingRecommendation array SizingRecommendation is the per-container worker resource recommendation derived from measured per-job usage peaks (Q359 Phase 2), refreshed by the AGC as jobs complete. Advisory only: nothing is applied to worker pods — the operator (or a future opt-in sizing profile) acts on it. It doubles as the persistence for the usage aggregates: the AGC re-seeds its in-memory history from this field on restart, so the observation window survives control-plane rollouts (status is the store; no separate backing store). Optional: {}
observedGeneration integer ObservedGeneration is the .metadata.generation the most recent reconcile acted on. Optional: {}

RunnerTemplate

RunnerTemplate is a namespace-scoped, reusable worker pod shape referenced by RunnerSets via templateRef. Pure data: nothing owns it and it owns nothing (§H.8).

Field Description Default Validation
apiVersion string actions-gateway.com/v2beta1
kind string RunnerTemplate
metadata ObjectMeta Refer to Kubernetes API documentation for fields of metadata.
spec RunnerTemplateSpec
status RunnerTemplateStatus

RunnerTemplateSpec

RunnerTemplateSpec is the worker pod shape shared by RunnerTemplate (namespaced) and ClusterRunnerTemplate (cluster-scoped). It is the only v2 object permitted to be large: isolating the PodTemplateSpec here keeps it out of the controller objects it would otherwise co-bloat, and lets one template be referenced by many RunnerSets (docs/design/appendix-h-v2-api-decomposition.md §H.2, §H.5).

Reserved pod-field guardrail (§H.4, §H.7). A worker pod's identity and isolation are controller-enforced invariants — in v1 the AGC silently overrode them at pod build time. v2 makes them author-time rejections instead, so a template that tries to set them fails closed rather than being silently rewritten. The cheap, scalar pod-level fields are enforced here via CEL; the per-container checks that require iterating an unbounded containers array (privileged containers, proxy env vars) exceed the CEL cost budget and move to the RunnerTemplate admission webhook in M2 (the milestone that adds the data-kind reconcilers).

Appears in: - ClusterRunnerTemplate - RunnerTemplate

Field Description Default Validation
podTemplate PodTemplateSpec PodTemplate is the standard Kubernetes PodTemplateSpec for worker pods — the large field this kind exists to isolate.
On the namespaced (tenant-authored) RunnerTemplate, podTemplate.spec.priorityClassName is gated by the GMC validating webhook against the platform PriorityClass allowlist (--allowed-priority-classes; empty forbids every named class). PriorityClass is cluster-scoped and sets the scheduler's preemption order cluster-wide, so it is the platform's to grant, not the tenant's (Q132/Q289). The cluster-scoped ClusterRunnerTemplate is platform-authored and therefore exempt.
workerImage string WorkerImage is the fully-qualified container image for the runner container. Optional: {}

RunnerTemplateStatus

RunnerTemplateStatus is the observed state of a (Cluster)RunnerTemplate. The templates are pure data with no reconciler, so the contract fields are present for uniformity across all five v2 kinds (§H.7) rather than being populated — no condition type is set on a template today (Q309).

Appears in: - ClusterRunnerTemplate - RunnerTemplate

Field Description Default Validation
conditions Condition array Conditions are the observed conditions of the template. No condition types are populated today: the templates are pure data with no reconciler; the field exists for the uniform v2 status contract. Optional: {}
observedGeneration integer ObservedGeneration is the .metadata.generation the most recent reconcile acted on. Optional: {}

ScaleUpRateLimit

ScaleUpRateLimit configures the opt-in per-RunnerSet worker-pod creation-rate limit (Q223): a token bucket where MaxPerSecond is the sustained refill rate and Burst is the bucket depth (the largest instantaneous batch before throttling engages). When the bucket is empty, an acquired job waits — holding its GitHub job lock, renewed in the background — until a token frees, composing with the namespace-quota retry wait rather than adding a new state machine.

Appears in: - RunnerSetSpec

Field Description Default Validation
maxPerSecond integer MaxPerSecond is the sustained rate, in worker pods created per second, once the initial burst is spent. Required when scaleUp is set. Maximum: 10000
Minimum: 1
burst integer Burst is the maximum number of worker pods that may be created in an instantaneous batch before the MaxPerSecond rate throttles subsequent creations — the token-bucket depth. Defaults to MaxPerSecond when omitted. Maximum: 100000
Minimum: 1
Optional: {}

SignerProvider

Underlying type: string

SignerProvider is the discriminator of the ExternalSigner union: it names the external signing backend. Vault (HashiCorp Vault transit) is the only provider in the MVP; cloud KMS backends (AWS/GCP/Azure) join as additive providers behind the same signer interface, by extending this enum and the union — no breaking change.

Validation: - Enum: [Vault]

Appears in: - ExternalSigner

Field Description
Vault SignerProviderVault selects a HashiCorp Vault transit signer (the MVP). Configured by ExternalSigner.Vault.

TracingConfig

TracingConfig configures opt-in OpenTelemetry distributed tracing for this tenant's AGC. Tracing stays off unless Endpoint is set. Unchanged from v1alpha1.

Authentication headers are intentionally not exposed: they can carry bearer tokens, and this project keeps secrets out of environment variables. Authenticate the collector at the network layer instead. See docs/operations/observability.md.

Appears in: - ActionsGatewaySpec

Field Description Default Validation
endpoint string Endpoint is the OTLP/gRPC collector address. Setting it enables tracing on the AGC; leaving it empty keeps tracing off. Maps to OTEL_EXPORTER_OTLP_TRACES_ENDPOINT. Optional: {}
insecure boolean Insecure disables TLS for the OTLP/gRPC connection. Defaults to false (TLS required). Maps to OTEL_EXPORTER_OTLP_TRACES_INSECURE. Optional: {}
sampler string Sampler selects the trace sampler. When empty the SDK default applies. Maps to OTEL_TRACES_SAMPLER. Enum: [always_on always_off traceidratio parentbased_always_on parentbased_always_off parentbased_traceidratio]
Optional: {}
samplerArg string SamplerArg is the argument for the chosen Sampler — for ratio-based samplers the sampling probability in [0,1]. Maps to OTEL_TRACES_SAMPLER_ARG. Optional: {}
resourceAttributes object (keys:string, values:string) ResourceAttributes are extra OpenTelemetry resource attributes merged onto every AGC span. Maps to OTEL_RESOURCE_ATTRIBUTES. Optional: {}

VPAUpdateMode

Underlying type: string

VPAUpdateMode selects how a managed VerticalPodAutoscaler actuates its recommendation. It is the subset of the upstream autoscaling.k8s.io updateMode values this API exposes.

The upstream value Auto is deliberately NOT offered: it is an alias whose actuation mechanism is version-dependent (today it evicts like Recreate; in-place resize is still alpha upstream), so a gateway pinned to Auto would silently change restart behavior on a VPA upgrade. Naming Recreate makes the eviction explicit.

Validation: - Enum: [Off Initial Recreate]

Appears in: - AGCVerticalAutoscaling

Field Description
Off VPAUpdateModeOff is recommendation-only: the VerticalPodAutoscaler observes the AGC and publishes a recommendation in its own status, but never mutates a pod. It is the default for agcAutoscaling so that opting in is observable before it is disruptive — nothing about the running AGC changes.
Initial VPAUpdateModeInitial applies the recommendation only at pod creation. A running AGC pod is never evicted by the autoscaler; it picks up a new size the next time it restarts for another reason (image change, node drain, spec edit).
Recreate VPAUpdateModeRecreate lets the autoscaler evict the AGC pod to apply a new recommendation. The AGC tolerates this: on SIGTERM in-flight listener goroutines deregister their sessions and the AGC re-registers them within GitHub's 2-minute redelivery window on restart (docs/design/appendix-e-capacity-planning.md §E.9). It is still a control-plane restart, so it is opt-in rather than the default.

VaultKubernetesAuth

VaultKubernetesAuth configures Vault Kubernetes auth: the AGC presents its projected ServiceAccount token, Vault verifies it against the cluster's token review API, and returns a short-lived Vault client token bound to Role. The pod-to-role binding is configured in Vault by the operator, out of band — only its name is named here.

Appears in: - VaultSigner

Field Description Default Validation
role string Role is the Vault Kubernetes auth role the AGC logs in as. The operator binds this role to the AGC ServiceAccount and namespace in Vault. MaxLength: 255
MinLength: 1
mount string Mount is the path the Vault Kubernetes auth method is mounted at. Optional; defaults to "kubernetes", Vault's conventional mount path. kubernetes MaxLength: 255
Optional: {}

VaultSigner

VaultSigner configures a HashiCorp Vault transit signer: the AGC authenticates to Vault with its pod identity (Vault Kubernetes auth) and asks Vault transit to sign the App JWT with a key Vault holds. No key or token is stored in the cluster — the AGC's projected ServiceAccount token is its only credential, and it is minted by the kubelet, not stored in a Secret.

Appears in: - ExternalSigner

Field Description Default Validation
address string Address is the Vault API base URL (e.g. https://vault.vault.svc:8200). HTTPS is required for production because the AGC's ServiceAccount token transits this channel at login; a plaintext address is permitted only under an explicit dev/test opt-in in the AGC (mirroring the GitHub token-exchange channel, docs/design/05-security.md). MaxLength: 2048
MinLength: 1
Pattern: ^https?://
transitMount string TransitMount is the path the Vault transit secrets engine is mounted at. Optional; defaults to "transit", Vault's conventional mount path. transit MaxLength: 255
Optional: {}
keyName string KeyName is the name of the Vault transit key that signs the App JWT. The key must be an RSA key (GitHub App keys are RSA); transit signs it as RS256 (pkcs1v15 + sha2-256). MaxLength: 255
MinLength: 1
auth VaultKubernetesAuth Auth configures how the AGC authenticates to Vault (Vault Kubernetes auth in the MVP).
networkPolicy EgressPeer NetworkPolicy optionally tells the GMC how to reach Vault as a NetworkPolicy egress peer (Q202). On a policy-enforcing CNI the per-tenant AGC NetworkPolicy default-denies egress (DNS + GitHub + the kube API server); Vault is not otherwise expressible as a peer because Address is an opaque URL, so set this and the GMC emits a scoped AGC→Vault egress rule on the Vault API port (parsed from Address). Leave it unset on a non- enforcing CNI (e.g. kindnet) or when the egress rule is managed out of band — the rule is a strict tightening that is only ever added, never a broaden-to-all-egress. Set on a GitHubApp gateway it has no effect (no Vault egress is emitted for the possession model).
The value is a shared EgressPeer (Q204): the selector/CIDR peer the GMC scopes the rule to. Its Port is left unset for Vault (the port is derived from Address); set it only to override that derived port.
Optional: {}

WorkerSizing

WorkerSizing configures the opt-in measured worker sizing profile (Q359 Phase 3). Applied by the AGC at pod-build time on every acquired job, so a spec edit takes effect on the next job without a restart (Q117).

Appears in: - RunnerSetSpec

Field Description Default Validation
profile string Profile selects the sizing behavior; see the SizingProfile* constants. Static Enum: [Static Binpack Throughput NodeShare]
Optional: {}
limitHeadroomPercent integer LimitHeadroomPercent (Throughput only) scales the observed per-job memory peak into the derived memory limit: limit = peak × percent / 100. Defaults to 150 (the dogfood-validated OOM-headroom band, rounded up for the burst-friendly profile). Memory only — no CPU limit is ever derived. Maximum: 1000
Minimum: 100
Optional: {}
nodeShare NodeShareSizing NodeShare declares the per-node envelope the NodeShare profile divides. Required when (and only meaningful when) profile is NodeShare. Optional: {}
minRequests ResourceList MinRequests / MaxRequests clamp every derived cpu/memory request (and the limits that track them) within an operator-set floor/ceiling, bounding how far a skewed usage history can push the derived values. Keys other than cpu and memory are ignored. Optional: {}
maxRequests ResourceList Optional: {}

WorkloadIdentity

WorkloadIdentity is the workload-identity credential member: the App's identity (appId/installationId, non-secret) plus an external signer that holds the App private key outside the cluster and signs the App JWT on the AGC's behalf. There is no privateKey field by design — the whole point of this method is that the key never enters the cluster (§H.15, docs/design/05-security.md).

Appears in: - GitHubCredentials

Field Description Default Validation
appId integer AppID is the GitHub App's numeric ID (the JWT issuer). Non-secret; inline rather than in a Secret because it identifies, not authenticates. Minimum: 1
installationId integer InstallationID is the GitHub App installation's numeric ID. Non-secret; inline. Minimum: 1
signer ExternalSigner Signer configures the external signer that signs the App JWT. The signing key lives in the signer's trust anchor (Vault transit in the MVP), never in the cluster.