AGN Operations
On this page
This guide covers AGN production installation, network formation, site lifecycle, routing configuration, security, and observability. Development-only orchestration is isolated under Development Validation Environments.
1. Deploy the AGN Operator
Install
AGN provides a Helm chart and Kustomize manifests for the operator and CRDs.
Option 1: Helm (recommended)
helm install grid-operator \
oci://ghcr.io/praxis-proxy/charts/grid-operator \
--version <version> \
--namespace grid-system \
--create-namespace
To grant resource access to additional namespaces:
helm upgrade grid-operator \
oci://ghcr.io/praxis-proxy/charts/grid-operator \
--set "resourceNamespaces={app-ns,data-ns}" \
--namespace grid-system
The chart installs and upgrades the CRDs. A platform that owns them sets
crds.enabled: false. The chart README covers adopting CRDs that an older
release installed.
Uninstalling the chart removes namespaced resources but retains CRDs. Custom resources created by other chart releases (e.g., grid-site) are not affected. See the chart README for the full values reference.
Option 2: Kustomize
The install manifests are generated from the chart (make generate-deploy; CI runs
make deploy-check). deploy/operator/ is the platform base: no Namespace, the image set
through params.env, metrics TLS from the enrolled site identity, no platform assumptions.
deploy/overlays/openshift/ is its OpenShift flavor, the base values plus a delta: metrics
TLS from the service CA and a NetworkPolicy. A platform operator vendors the base or a
flavor as a kustomize base and sets the namespace in its own kustomization. The SWIM and
signals Services are LoadBalancer, the only type the operator accepts for the Service it
advertises, so the cluster needs a LoadBalancer provider. deploy/overlays/kind/ is the
standalone install with its own grid-system Namespace and ClusterIP Services.
# Standalone deployment (CRDs + operator, kind shape)
kubectl apply -k deploy/
# Or step-by-step:
kubectl apply -k deploy/crds/
kubectl apply -k deploy/overlays/kind/
Option 3: Generated CRDs (development)
# Generate CRDs from source and apply directly
cargo run -p operator --bin generate_crds | kubectl apply -f -
kubectl apply -k deploy/overlays/kind/
For regenerating CRDs after schema changes:
make generate-crds
This writes deploy/crds/ and charts/grid-operator/templates/crds/. CI runs
make crds-check, which fails when the committed CRDs no longer match the
Rust types.
Container image pattern: The operator Containerfile uses a
rust:1.96-alpine builder with BuildKit cache mounts, and an alpine:3.23
runtime with a non-root user and no build toolchain. The Kubernetes Deployment
adds a restricted security context for OpenShift-style clusters.
Image availability: Production deployments pin project-owned release or commit-digest images. Local image loading is documented under Development Validation Environments.
Deployment Examples
See sample Custom Resource configurations:
config/samples/- standard operator sample CRsdeploy/examples/single-cluster-api-provider/- minimal example with external API
The standalone install creates:
| Resource | Name | Scope |
|---|---|---|
Namespace | grid-system | cluster (kind overlay only) |
ServiceAccount | grid-operator, grid-operator-metrics-scraper | grid-system |
ClusterRole | grid-operator-crd, grid-operator-metrics-scraper, grid-operator-resources (verb definitions only) | cluster |
ClusterRoleBinding | grid-operator, grid-operator-metrics-scraper | cluster |
RoleBinding | grid-operator (resources access) | grid-system, plus default in the kind overlay |
Role and RoleBinding | grid-operator-metrics-scraper-token | grid-system |
Service | grid-operator-metrics, grid-operator-swim | grid-system |
NetworkPolicy | grid-operator | grid-system (platform base only) |
Deployment | grid-operator | grid-system |
The operator runs as a single binary with multiple
controllers (one per CRD type) in the same process. No
SWIM runtime starts until a GridNetwork resource exists.
Important: AGN deploys only the operator and CRDs. Cluster lifecycle, Praxis AI gateways, inference runtimes, load-balancer integrations, and DNS remain deployment-platform responsibilities.
Praxis AI gateway deployment is separate and requires:
- Praxis AI image with required filters (
intelligent_route,credential_inject) - Consumer gateway configuration referencing AGN-generated ConfigMaps
- Provider gateway deployment with AGN-compatible endpoints
Operator image
The intended project-owned operator image path is:
ghcr.io/praxis-proxy/grid-operator
Repository CI publishes source-SHA images, and the release workflow publishes versioned images with SBOM and provenance attestations.
Tag policy:
| Tag | Mutability | When pushed |
|---|---|---|
sha-<7-char-commit> | Immutable | Used once publishing is enabled |
v<version> | Immutable release tag | Used once releases are cut |
Deployments should pin an immutable digest:
image: ghcr.io/praxis-proxy/grid-operator@sha256:8c8271aa589fbd81e346b75ae580be9e8085c3b283b4e6a99e2b9adcea73e12d
Override: set grid-operator-image in deploy/operator/params.env (platform
base; a kustomization that vendors the base uses the images transformer instead,
since a merged parameters ConfigMap arrives after the base’s replacement has run), or
set image in deploy/overlays/kind/values.yaml and run make generate-deploy, or
patch the Deployment after apply:
kubectl set image deployment/grid-operator \
-n grid-system \
operator=ghcr.io/praxis-proxy/grid-operator:sha-<commit>
Registry namespace: the image is published under ghcr.io/praxis-proxy/.
CI publishing setup: the publish job in
.github/workflows/helm.yaml publishes source-SHA images from main, using
the images the e2e jobs tested. .github/workflows/release.yaml publishes version and source-SHA tags
after the tagged source passes release gates.
Security: the operator image contains only the statically linked operator binary. No secrets, tokens, SWIM encryption keys, or credentials are baked into the image. Production publication requires an SBOM and image signing in the release pipeline.
RBAC permissions
RBAC is split into two ClusterRoles:
grid-operator-crd: cluster-scoped CRD access, bound via aClusterRoleBinding.grid-operator-resources: namespacedSecretandConfigMapaccess, bound via per-namespaceRoleBindings.
The default install includes a RoleBinding in the
default namespace only. All mutations use server-side
apply (patch). SSA on a non-existent resource requires
create permission, so both create and patch are
granted for secrets and configmaps. delete and
update are not granted.
AGN CRDs (cluster-scoped, grid-operator-crd):
| Resource | Verbs | Why |
|---|---|---|
gridnetworks | get, list, watch, patch | Controller watch loop; SSA spec/status writes |
gridnetworks/status | get, patch | Phase, connectedSites, remoteProviderCount |
gridsites | get, list, watch, patch | Controller watch; auto-creation from SWIM Alive members |
gridsites/status | get, patch | Phase, reason, publicCertPem, observedGeneration |
inferenceproviders | get, list, watch, patch | Controller watch; site-selector matching |
inferenceproviders/status | get, patch | Phase, matchingSites, observedGeneration |
Events (events.k8s.io, grid-operator-resources):
| Resource | Verbs | Why |
|---|---|---|
events | create, patch | Published on GridSite phase/reason transitions with action GatewayProbe |
GridSite is cluster-scoped, so its events land in the operator namespace:
kubectl -n <operator namespace> get events --field-selector involvedObject.kind=GridSite.
Core resources (namespaced, grid-operator-resources):
| Resource | Verbs | Why |
|---|---|---|
secrets | get, create, patch | Read TLS certs, SWIM key, credential refs; SSA-create CA and site cert Secrets |
configmaps | get, create, patch, update | Routing overlay, consumer config, and Secret requirements ConfigMaps; SWIM revision reservations |
The grid-operator-resources ClusterRole is never bound
cluster-wide. It takes effect only in namespaces where a
RoleBinding references it.
Deployment access
Neither ClusterRole grants unrestricted Deployment access. With rotation
on, the resources ClusterRole grants get and patch on the named gateway
Deployment. The operator reaches Deployments through two named grants only:
| Grant | Bound where | Why |
|---|---|---|
deployments get, patch on gateway.serviceName (Helm chart only, with rotation on) | Release namespace, each resourceNamespaces entry, and gateway.namespace through the discovery Role | Roll the gateway after an identity rotation |
deployments get, patch on one delegated gateway’s Deployment | That gateway’s namespace, through a Role its owner creates | Delegated Secret mount reconciliation |
A gateway that delegates its mounts (praxisConfig.mountReconciliation
on the GridNetwork, mountReconciliation.enabled on the praxis-gateway
chart) is the one consenting to the operator patching its Deployment, so
its chart renders the Role: resourceNames is the chart’s Deployment
name, fullnameOverride moves both together, and the RoleBinding names
the operator ServiceAccount from mountReconciliation.operator
(grid-operator, whose namespace has no default, since a Helm and a
platform install put the operator in different ones). For a Deployment the chart
did not install, or with mountReconciliation.rbac.create: false, create
the equivalent by hand, substituting the namespace the operator runs in:
grid-system in this example, odh-ai-grid under OpenDataHub and
redhat-ai-grid under Red Hat OpenShift AI.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: inference-gw-mount-reconciliation
namespace: praxis-system # the gateway namespace
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames: ["inference-gw"] # praxisConfig.mountReconciliation.deploymentName
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: inference-gw-mount-reconciliation
namespace: praxis-system
subjects:
- kind: ServiceAccount
name: grid-operator
namespace: grid-system # the operator namespace, not a default
roleRef:
kind: Role
name: inference-gw-mount-reconciliation
apiGroup: rbac.authorization.k8s.io
The installer needs permission to create or update Roles and RoleBindings
in the gateway namespace. Kubernetes also checks role escalation: the installer
must hold the granted get and patch permissions on that named Deployment,
or have escalate on the Role. Binding requires the Role’s permissions or
bind on that Role. Namespace edit access lets a principal deploy a gateway
but does not grant permission to create these RBAC objects. If the installer
lacks these permissions, set mountReconciliation.rbac.create: false and
have an authorized administrator create the grant separately.
What the grants still allow, as RBAC facts rather than what the operator chooses to do:
patchon the delegated Deployment is whole-object: the pod template, image, ServiceAccount, and anything else in the Deployment, not only mounts. The opt-in annotations the operator checks before patching, and the strategic patches it builds, are its own safety checks, not an RBAC restriction.- The grant lasts as long as the gateway chart keeps
mountReconciliation.enabled, whether or not theGridNetworkstill delegates. - The rotation grant on
gateway.serviceNameapplies in every namespace the resources ClusterRole is bound in, whether or not that namespace holds the gateway. - Secrets
get,create,patch; ConfigMapsget,create,patch,update; and Eventscreate,patchin every bound namespace, as before.
Without the grant, the operator gets a 403 on the Deployment read and the
gateway’s mountReconciliationStatus[] entry reports
reason: DeploymentForbidden, naming the verb it was refused. Nothing is
patched, the previous populated routing overlay is retained, and new
populated routes stop publishing to that gateway until the grant exists. If
the last provider disappears, an empty authoritative overlay is still
published before mount reconciliation so watched routes stop selecting
providers. A deploymentName outside the name-restricted grant also produces
403. If the name is authorized but the Deployment does not exist, the read
returns 404 and status reports DeploymentMissing.
Upgrading from v0.2.0 with delegation enabled
v0.2.0 granted deployments get, patch without resourceNames in the
resources ClusterRole. That rule is gone, so a v0.2.0 install with
praxisConfig.mountReconciliation.enabled upgrades in this order:
- Upgrade the praxis-gateway chart, or create the Role above, for every delegated gateway.
- Upgrade the operator chart, or re-apply
deploy/operator/.
Out of order, step 2 alone leaves delegated Deployments outside any remaining
rotation grant inaccessible until step 1 lands: the DeploymentForbidden
status above, and no patches in between.
Secret access rules
The operator reads Secrets in the namespace declared by
each SecretRef in the CRD spec. It does not search
across namespaces or list Secrets.
| Secret path | Keys read | Keys written |
|---|---|---|
spec.tls.siteSecretRef | tls.crt, tls.key (client cert + private key for mTLS gateway probes; key bytes wrapped in Zeroizing) | tls.crt, tls.key (create-if-absent via SSA patch) |
spec.tls.caSecretRef | ca.crt (existence check) | ca.crt, ca.key (create-if-absent via SSA patch) |
spec.tls.swimKeySecretRef | key (or custom key field) | None |
spec.auth.secretRef | existence + UTF-8 validation | None |
Secret writes use SSA patch with field manager
grid-operator. SSA on a non-existent resource requires
both create and patch permission in the target
namespace.
Credential token bytes are never written to ConfigMaps,
overlays, status fields, or logs.
ConfigMap write scope
ConfigMap | Naming | Data key | Namespace |
|---|---|---|---|
| Routing overlay | grid-overlay-{network}-{gateway} | routing-overlay.json, routing-config.json | GatewayRef.namespace |
| Consumer config | praxisConfig.configMapName | praxis.yaml | GatewayRef.namespace |
| Mount requirements | grid-mount-requirements-<hash> | mount-requirements.json | GatewayRef.namespace |
What is not granted
Neither ClusterRole grants:
pods,pods/exec,pods/log,pods/portforwardservices,ingressessecretsdelete,list,watchconfigmapsdelete,list,watchdeploymentsupdate,delete,list,watch, andget,patchon any Deployment outside the two named grants above
Adding namespaces
The default install grants Secret and ConfigMap access
only in the default namespace. To grant access in
additional namespaces (e.g. the gateway namespace
referenced by GatewayRef, or the namespace holding TLS
Secrets), create a RoleBinding in each:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: grid-operator-resources
namespace: praxis-system # the target namespace
subjects:
- kind: ServiceAccount
name: grid-operator
namespace: grid-system # the operator namespace, not a default
roleRef:
kind: ClusterRole
name: grid-operator-resources
apiGroup: rbac.authorization.k8s.io
Add one RoleBinding per namespace referenced by
GatewayRef.namespace, tls.caSecretRef.namespace,
tls.caConfigMapRef.namespace, tls.siteSecretRef.namespace,
tls.swimKeySecretRef.namespace, and auth.secretRef.namespace in your CRD specs.
Deployment configuration
The generated Deployment (deploy/operator/deployment.yaml, rendered from
charts/grid-operator) exposes SWIM configuration through environment variables:
SWIM endpoint values accept all of these forms:
10.0.0.4:7946
[2001:db8::4]:7946
grid-swim.example.internal:7946
DNS names are resolved before the SWIM runtime starts. All usable seed addresses are retained, sorted, and deduplicated. For an advertised hostname, the first address in that deterministic ordering becomes the concrete address published to SWIM. Resolution is bounded; an invalid or unresolvable advertise endpoint prevents SWIM startup rather than silently advertising an unrelated address.
| Variable | Purpose |
|---|---|
GRID_SWIM_BIND_ADDR | UDP address to bind the SWIM listener |
GRID_SWIM_ADVERTISE_ADDR | Advertised SWIM endpoint; accepts ip:port, [ipv6]:port, or hostname:port and defaults to $(POD_IP):7946 |
GRID_SWIM_SERVICE_NAME | SWIM Service in the operator namespace, which must be type LoadBalancer. The operator advertises its LoadBalancer address, waiting for it without a fallback and reporting not ready until then. Controllers run during the wait, and writes derived from membership wait for a first peer or a 15 second grace. When three polls in a row no longer list the advertised hostname or IP, the operator leaves the cluster and exits, so the restarted pod advertises the new one |
GRID_SWIM_ADVERTISE_FALLBACK | Marks a GRID_SWIM_ADVERTISE_ADDR equal to it as the Pod IP default. The operator then builds the Pod IP endpoint itself, bracketing IPv6, or discovers the LoadBalancer address when GRID_SWIM_SERVICE_NAME names a Service. An older operator ignores it and advertises the Pod IP |
GRID_SWIM_REQUIRE_KEY | Hold all SWIM traffic until the GridNetwork key loads or the network declares none. Defaults to true. With false, SWIM is plaintext while no network exists |
GRID_SWIM_SITE_NAME | Unique site identity for this operator instance |
GRID_SWIM_SEEDS | Comma-separated SWIM seed endpoints; accepts ip:port, [ipv6]:port, or hostname:port |
GRID_SIGNALS_ADDR | Signals listener address. Defaults to [::]:9091, or 0.0.0.0:9091 on a host without IPv6 |
GRID_SIGNALS_ADVERTISE_ADDR | Signals endpoint gossiped to peers, as ip:port, [ipv6]:port, or hostname:port. Without it, a site advertising the SWIM LoadBalancer gossips that address on the Service port named signals, and any other site gossips none. A LoadBalancer site needs one of the two, and the Service port needs a LoadBalancer that serves UDP and TCP on one Service |
GRID_SIGNALS_PEER_PORT | Port dialed on a peer’s SWIM host when the peer gossips no signals endpoint. Defaults to 9091 |
GRID_SIGNALS_MAX_PER_PEER | Concurrent authenticated signals connections one peer site may hold. Defaults to 8 |
GRID_GATEWAY_ADDRESS | Explicit gateway address override (skips self-discovery) |
GRID_GATEWAY_DISCOVERY_ENABLED | Whether to discover and advertise a gateway address. Defaults to true; set false for a consumer-only site |
GRID_GATEWAY_SERVICE_NAME | Service name for gateway self-discovery (default: provider-gateway) |
GRID_GATEWAY_NAMESPACE | Namespace for gateway Service lookup (default: grid-system) |
GRID_GATEWAY_PORT | Optional port override appended to the discovered address. When unset, use the first Service spec.ports entry; fall back to 8080 if no usable port exists |
GRID_GATEWAY_DISCOVERY_INTERVAL_MS | Polling interval for gateway discovery (default: 5000) |
Gateway port discovery reads only the gateway Service’s spec.ports; Pod health
probes and metrics endpoints are not used for this selection.
The signals listener caps handshakes per client source address. Behind a
LoadBalancer, set externalTrafficPolicy: Local on the Service that carries
the signals port. With Cluster, node SNAT gives many clients one source
address, and they share its cap.
GRID_SWIM_ENCRYPT_KEY is intentionally omitted from the
Deployment. Production SWIM encryption uses
GridNetwork.spec.tls.swimKeySecretRef to reference a
Kubernetes Secret. The env var exists for local
development and testing only.
Validate the install
cargo xtask env verify-operator-install-rbac \
-c tests/env/operator-routing.toml
This command builds the operator image, loads it into a
Kind cluster, applies the install manifests, runs positive
and negative kubectl auth can-i checks (including
namespace-scope proofs), then waits for the
in-cluster Deployment to reconcile a test GridNetwork
using only the installed ServiceAccount.
2. Create a GridNetwork
apiVersion: grid.praxis.fast/v1alpha1
kind: GridNetwork
metadata:
name: production
spec:
seeds:
- "10.0.0.5:7946"
consumerGateways:
- name: inference-gw
namespace: praxis-system
tls:
caSecretRef:
name: grid-ca
namespace: praxis-system
siteSecretRef:
name: grid-site-cert
namespace: praxis-system
The GridNetwork controller:
- Generates a grid CA via
certs - Generates this site’s certificate (DNS SAN:
{site-name}.grid.internal, dual EKU for mTLS) - Stores both in Kubernetes Secrets
- Starts the SWIM runtime with seed peers
- Sets
status.phase: Initializing
CRD-driven seeds
spec.seeds is operator-consumed: on every GridNetwork reconcile the
controller parses and resolves each entry independently, logs invalid or
temporarily unresolvable entries at warn, retains successful addresses,
removes the local advertise address to prevent self-announce noise,
deduplicates, and calls SwimHandle::announce_seeds to deliver the batch to
the running SWIM event loop. Re-announcing to already-connected peers is
idempotent: foca ignores redundant joins. If every non-empty configured seed
fails, the controller retains the last-known-good resolved set; if none exists,
SWIM remains active with a seedless bootstrap and a degradation warning.
Startup seeds from GRID_SWIM_SEEDS (env var) and CRD seeds are additive.
The env var seeds are applied once at startup; CRD seeds are applied on every
reconcile, so dynamically added addresses take effect without an operator restart.
Runtime update contract
Change to spec.seeds | Effect |
|---|---|
| Seed added | Announced to SWIM on the next reconcile; join initiated |
| Seed removed | Logged; no active disconnect (SWIM failure detection ages the peer out naturally) |
| Seeds unchanged | Re-announced idempotently; no side effects |
Adding a seed requires no operator restart. The new address is SWIM-joined within one reconcile cycle (~300 s default requeue, or sooner if a watch event fires).
Removing a seed does not disconnect the peer. The removed peer remains in SWIM
membership until it stops responding to probes and is declared Suspect then
Dead by the SWIM protocol.
Global-runtime semantics
The SWIM runtime is process-global: one UDP listener per operator process,
shared across all GridNetwork reconciles. Seeds from any
GridNetwork.spec.seeds are announced to the same SWIM membership node.
This is site-membership bootstrap, not per-network membership isolation.
CRDT provider records remain network-scoped separately.
Each membership identity carries a restart-scale u64 generation. A
replacement process at the same advertised address presents a greater
generation and supersedes the retained process identity; in-process renewals
increment it without wrapping. All peers in one SWIM membership domain must
run a wire-compatible identity schema. A release that changes that schema
must be coordinated across the membership domain rather than deployed as an
unqualified rolling update.
Transport-security contract
SWIM is the AGN control-plane membership and state broadcast channel. When
spec.tls.swimKeySecretRef is configured and the referenced Secret resolves to a
valid 32-byte key, reconcile applies the key before announcing CRD seeds or
publishing certificate/provider state. From that point, outgoing SWIM UDP
packets are encrypted and authenticated with AES-256-GCM. Incoming packets
that fail authentication are silently dropped; the foca membership state
machine never sees them.
When swimKeySecretRef is absent, SWIM traffic is sent and received as cleartext
(backward-compatible local and development behavior).
If swimKeySecretRef is configured but the Secret is missing, unreadable, or not a
valid 32-byte key, the reconcile fails before CRD seed announcement and
certificate/provider broadcasts for that GridNetwork. The SWIM runtime is
process-global, so a previously loaded key remains active until restart; the
operator does not switch to plaintext for that configured reconcile.
GRID_SWIM_ENCRYPT_KEY is the local and Kind validation path for startup-time
enforcement because it is available before the UDP socket starts. It is
process environment material and should not be treated as the production Secret
delivery mechanism. With CRD-backed swimKeySecretRef, the key is applied at
GridNetwork reconcile time; use the environment key as well when startup-time
plaintext acceptance must be avoided before CRD preload support exists.
SWIM encryption protects: gossip membership packets, gateway address broadcasts, public certificate PEM broadcasts, and CRDT provider state broadcasts.
SWIM encryption does not protect: data-plane request traffic. Gateway request-time authentication and authorization are enforced by Praxis/Praxis AI gateway TLS and peer identity filters, not by SWIM membership.
Routing eligibility remains fail-closed at the GridSite layer independently of
SWIM encryption: remote CRDT provider records are rendered only for peers whose
GridSite is Active. Active indicates control-plane eligibility: sufficient
trust information to include the site in routing overlays. Data-plane readiness
is verified separately. Both layers are required for production deployments.
Channel-full retry
If the seed announce channel is full (capacity 16 batches), the announce is
skipped for the current reconcile and retried on the next
(REQUEUE_INTERVAL = 300 s). Seeds are not guaranteed to be applied
immediately under heavy broadcast load.
Seed format
Seeds accept IP:port, [IPv6]:port, and hostname:port, for example
10.0.0.2:7946 or grid-swim.example.internal:7946. DNS is resolved during
each reconciliation with per-endpoint and aggregate bounds; results are sorted
and deduplicated. Invalid or failed entries are skipped with source and
endpoint diagnostics. A partial result is announced, while a wholly failed
non-empty list does not replace a working last-known-good set.
Troubleshooting seed changes
New seed not joining:
- Verify the address is a valid
IP:port,[IPv6]:port, orhostname:port. - Check the operator log for
announcing CRD seeds to SWIM runtimeornew CRD seeds added; if absent, the reconcile may not have fired yet. - Check for
failed to queue CRD seeds for SWIM announcementatwarnlevel, indicating a channel-full retry. - Verify the remote operator is running with
GRID_SWIM_BIND_ADDRset to the expected address.
Removed seed still shows as connected:
- Expected behavior. SWIM does not actively disconnect on seed removal.
- Wait for the WAN probe and suspicion window. The runtime probes every five
seconds, allows three seconds for a direct response before indirect probes,
and retains a suspect member for at least three probe periods before
declaring it
Dead.GridNetwork.status.connectedSitesthen decreases. - If the remote operator is still running, it will rejoin as
Aliveagain because SWIM membership is peer-to-peer and periodically announces to live and down members. Seeds bootstrap discovery; they are not an allowlist.
Phase progression: GridNetwork Active is set when
the SWIM runtime reports at least one Alive peer in
its MembershipSnapshot. Degraded is set when peers
are known but all are Suspect or Dead.
connectedSites reflects the live SWIM Alive peer
count; remoteProviderCount reflects remote
InferenceProvider records received via SWIM CRDT
broadcast.
Both fields are 0 and the phase remains Pending or
Initializing when SWIM is disabled (i.e. the operator
is started without GRID_SWIM_BIND_ADDR).
3. Sites Discover Each Other
When the SWIM runtime contacts a seed peer:
Grid ID negotiation:
- Neither site has a
gridId: deterministic tie-break (lexicographic site name), winner generates UUID, other adopts it - Remote has a
gridId, local doesn’t: local adopts it - Both have the same
gridId: normal join - Both have different
gridIds: connection rejected (separate grids)
The operator creates a GridSite resource for the
discovered peer.
GridSite status: phase: Discovered
4. Gateway Address and Trust Bootstrap
SWIM discovery proves that a peer is participating in gossip. It does not authorize that peer for request routing.
SWIM bootstrap phases
The trust bootstrap for a remote site progresses through these steps:
SWIM discovery: the peer is observed as Alive in SWIM membership. Phase:
Discovered. No trust established.Gateway address known: the remote operator advertises its resolved gateway address via SWIM state broadcast. The address is resolved by the self-discovery poller (Service LoadBalancer lookup) or from the
GRID_GATEWAY_ADDRESSoverride. The local operator stores it inGridSite.spec.gatewayEndpoint.address. Phase:Connecting. No trust established.Public cert material received: the remote operator broadcasts its public site certificate PEM. The operator validates the PEM structure (rejects private-key markers; checks for
CERTIFICATEheader) and stores it inGridSite.status.publicCertPem.Identity policy configured — set
spec.gatewayEndpoint.tls.serverNameto the expected DNS SAN and setspec.trust.canonicalFingerprintsto one or two independently verified DER-certificate SHA-256 pins. ConfigureGridNetwork.spec.tls.caSecretRefandsiteSecretReffor server and client authentication.Identity-aware gateway probe passes: the
GridSitecontroller performs a bounded mTLS handshake. It verifies the CA chain, DNS SAN, client authentication, and the canonical pin against the live leaf certificate. Success promotes the site toActivewith reasonTlsVerified.spec: gatewayEndpoint: address: provider.example.com:8443 tls: mode: Mutual serverName: provider.example.com trust: canonicalFingerprints: - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"See Authentication and Access Policy for the trust contract.
Data-plane mTLS enforced: a provider Praxis gateway validates peer identity over mTLS on every request, independent of the control-plane phase. Deployment acceptance requires positive and negative runtime probes; manifest inspection alone is not evidence.
Authentication vs authorization
| Concept | Question it answers | AGN mechanism |
|---|---|---|
| Authentication | “Is this peer really the site it claims to be?” | Identity-aware AGN health probe plus gateway mTLS peer validation on each request |
| Authorization | “Is this authenticated peer allowed to participate in this AI Grid Network or receive/send this traffic?” | Local AGN policy plus destination gateway enforcement |
A peer must satisfy both. A SWIM peer must never become routable solely because it gossiped successfully.
Security rules
- SWIM membership is discovery, not authorization.
- TCP reachability proves an address accepts connections, not identity.
publicCertPempresent means the PEM structure is valid and no private-key markers were detected. It does not prove the cert is signed by a trusted CA or that the peer is authorized.- Private keys, credential tokens, and Secret data must never be written to
GridSitestatus,GridNetworkstatus, overlays, generated ConfigMaps, or logs. - The operator does not copy Kubernetes Secrets across clusters as part of site discovery.
- The provider gateway still enforces peer identity on every request with mTLS,
independently of
publicCertPemstatus.
Routing eligibility
GridSite.status.phase == Active is the control-plane eligibility gate for remote CRDT provider records.
Provider records advertised by a SWIM peer are included in the routing overlay only when
the corresponding GridSite is Active. Peers in Discovered, Connecting, or any
other phase are excluded. Peers with no matching GridSite are also excluded (fail-closed).
GridSite Active is a control-plane eligibility signal. It proves the configured gateway health probe succeeded. It does not prove that Praxis loaded the latest routing config or authorized a particular request.
Setting Active in Mutual mode requires the configured CA, client identity,
server name, canonical pin, and live gateway certificate to agree. A provider
gateway independently authorizes peer identity on every data-plane request.
Active alone is not evidence that request authorization succeeded.
5. Connectivity Verification
The GridSite controller verifies gateway reachability and identity against
spec.gatewayEndpoint.address.
| Condition | Current check |
|---|---|
SWIMReachable | SWIM membership reports the peer Alive |
GatewayAddressKnown | spec.gatewayEndpoint.address is non-empty |
TlsVerified | Mutual TLS handshake, chain, SAN, and live-leaf pin all verify |
IdentityVerificationRequired | Plaintext endpoint accepts TCP, but remains ineligible because its identity is not verified |
Request-time authorization remains enforced by the provider gateway after the control-plane health evaluation succeeds.
6. Capability Negotiation
Sites publish capability and provider state through AGN control-plane records and CRDT-over-SWIM propagation. Capability information can include models, tools, agents, and provider availability signals.
The GridSite status capabilities field records broad site capability
classes. A site should only be treated as fully usable after discovery,
gateway reachability, trust establishment, and data-plane readiness are all
satisfied.
7. Register Providers
Users or auto-discovery create provider resources. See the CRDs doc for full specs.
Example for an API provider:
apiVersion: grid.praxis.fast/v1alpha1
kind: InferenceProvider
metadata:
name: anthropic-api
spec:
gridNetworkRef: production
providerKind: anthropic
backendKind: api_provider
endpoint: https://api.anthropic.com
models:
- name: claude-sonnet-4
auth:
strategy: bearer_token
secretRef:
name: anthropic-token
namespace: praxis-system
key: token
accessPolicy:
siteSelector: {}
Example for a local llm-d cluster:
apiVersion: grid.praxis.fast/v1alpha1
kind: InferenceProvider
metadata:
name: local-vllm
spec:
gridNetworkRef: production
providerKind: self_hosted
backendKind: local
endpoint: http://vllm-service.inference:8000
models:
- name: llama-3.2-8b
8. Routing Configuration
The GridNetwork controller renders routing overlay
ConfigMaps from CRD data. For each consumerGateways entry in the
GridNetwork, it server-side applies a ConfigMap
named grid-overlay-{network}-{gateway} containing:
routing-overlay.json: the versioned, content-addressed envelope consumed by Praxis AI. It includes scope, provenance, revision, digest, and the routing payload.routing-config.json: JSON-serialised legacyRoutingOverlaypayload with oneRoutingCandidateper model perInferenceProviderin the network. Whenspec.auth.secretRefis set, candidates carry only the credential reference, never token bytes.
The overlay shape is compatible with the Praxis
intelligent_route filter:
{
"network": "production",
"local_site": "production",
"candidates": [
{
"kind": "inference_model",
"name": "claude-sonnet-4",
"site": "anthropic-api",
"cluster": "anthropic-api",
"fresh": true,
"credential": {
"strategy": "bearer_token",
"secretRef": {
"name": "anthropic-token",
"namespace": "praxis-system",
"key": "token"
}
}
}
]
}
Cluster naming: candidate.cluster uses
spec.clusterName when set, otherwise the
InferenceProvider metadata name. The Praxis
load_balancer cluster serving that provider must use
the same identity.
Local development with xtask env maps overlay site
identities to generated gateway-{site} load-balancer
entries; see crates/xtask/src/env/operator_overlay.rs.
Routing overlay delivery
For gateways that must react promptly to provider health, capacity, or score
changes, enable the overlay-sync delivery path in the praxis-gateway chart.
operator reconcile
-> overlay ConfigMap applied
-> overlay-sync Kubernetes API watch
-> envelope validated
-> atomic write to shared emptyDir
-> Praxis file watcher hot-reloads
A direct ConfigMap volume is eventually refreshed by the kubelet. That is adequate for static or slowly changing configuration, but its refresh delay is not appropriate as the delivery mechanism for short-lived routing changes. The sidecar watches the API directly, so a published revision does not wait for the kubelet’s projected-volume polling cycle.
The sidecar adds correctness controls as well as lower latency:
- an init container blocks Praxis startup until the first valid overlay exists;
- schema version, destination scope, content digest, and maximum size are checked before publication;
- a temporary file plus
fsyncand rename prevents partial reads; - invalid updates, source deletion, and temporary API loss retain the last-known-good file;
- readiness, degraded state, accepted/rejected counters, write counters, and timestamps make the delivery boundary observable; and
- a dedicated ServiceAccount token is mounted only into the init and sidecar containers. Praxis has no Kubernetes API access.
The sidecar does not change the metrics scrape interval or force the AGN operator to reconcile. End-to-end convergence is still:
metrics become visible
+ operator scrape/reconcile
+ ConfigMap apply
+ sidecar API-watch delivery
+ Praxis file-watch reload
Use overlay.sidecar.enabled=false for the direct ConfigMap projection
fallback. Do not use that mode when a demo or production SLO assumes prompt
metrics-driven route changes.
9. Workloads Consume Providers
Workloads send requests to the Praxis Gateway. The gateway’s grid scoring filter selects the optimal backend. Praxis AI handles API translation and credential injection transparently.
For API-provider routes, the request-time path is:
intelligent_route
-> writes intelligent_route.credential.* metadata from the selected candidate
credential_inject
-> reads the matching token from a mounted Secret file
-> injects Authorization: Bearer <token>
load_balancer
-> forwards to the selected provider cluster
The token is not stored in the AGN overlay or consumer
Praxis ConfigMap.
For direct API-provider and cloud-provider fallback, the consumer gateway is often also the final-hop gateway, so the credential Secret is mounted there. For remote AGN sites, provider credentials should live only in the remote provider site or provider-side component that makes the final backend call. AGN carries the reference needed for routing and configuration; it does not copy Secret values between clusters.
The native path requires a Praxis AI image that includes the
credential_inject filter. AGN can render the
file-backed filter config today, but runtime deployments must
use an AI image with that filter merged and published.
See Auth & Policy for workload access patterns and authentication strategies.
GridSite trust bootstrap
Public certificate exchange
When a GridNetwork has spec.tls.siteSecretRef configured, the operator reads
the public site certificate (tls.crt) from that Secret on each reconcile and
broadcasts it to SWIM peers. Remote peers store the received certificate in
GridSite.status.publicCertPem.
To verify that a remote site’s public certificate has been received:
kubectl get gridsite <site-name> -o jsonpath='{.status.publicCertPem}'
A non-empty value means the remote operator is advertising structurally valid
public certificate material. To advance a Mutual TLS site to Active, configure
the CA and local client identity on the GridNetwork, then configure the
expected serverName and canonicalFingerprints on the GridSite.
A site in TrustMaterialMissing lacks at least one required CA, client
certificate, client key, server name, or canonical pin. Configure
spec.tls.siteSecretRef on the remote GridNetwork to enable certificate
advertisement, but do not derive trust solely from the advertised value.
Security rules
The public certificate recorded in status.publicCertPem is not automatically
trusted. The control plane records received trust material for operator visibility.
The provider gateway enforces mTLS peer identity and certificate validation on every
request; the control plane record does not bypass that check.
Private keys are never included in SWIM broadcasts. The operator reads only the
public certificate (tls.crt) from the site Secret, not the private key (tls.key).
GridSite gateway address configuration
The operator resolves and advertises its data-plane gateway address to SWIM
peers. This address is propagated through SWIM state broadcasts and used by
receiving operators to populate GridSite.spec.gatewayEndpoint.address for
auto-discovered sites.
Self-discovery (default): A background poller periodically looks up the
provider-gateway LoadBalancer Service and extracts its external IP. The
poller retries every 5 seconds (configurable via
GRID_GATEWAY_DISCOVERY_INTERVAL_MS) until the address appears, then
continues watching for changes. Discovered addresses are pushed to the SWIM
runtime via a watch channel.
Explicit override: Set GRID_GATEWAY_ADDRESS to skip the self-discovery
poller entirely.
Set GRID_GATEWAY_DISCOVERY_ENABLED=false on a consumer-only site that has no
local gateway address to advertise. The default is true. Disabling discovery
stops Service lookups and the poller, but an explicit GRID_GATEWAY_ADDRESS
still wins and is advertised.
Discovery uses the first LoadBalancer ingress hostname or IP. The advertised
port is GRID_GATEWAY_PORT when set, otherwise the first Service spec.ports
entry, with 8080 as the fallback if no usable port exists. A ClusterIP or
NodePort Service supplies no discovered address; use an explicit reachable
address for those Service types.
# Self-discovery (default): operator discovers from provider-gateway Service
GRID_GATEWAY_SERVICE_NAME=provider-gateway ./operator
# Explicit override: skip discovery poller
GRID_GATEWAY_ADDRESS=10.0.0.4:8080 ./operator
Requirements:
- Format:
host:portorIP:port(any non-empty string is accepted; the remote operator stores it verbatim inGridSite.spec.gatewayEndpoint.address) - When absent or empty and no LoadBalancer Service exists: auto-discovered
GridSiterecords have emptyspec.gatewayEndpoint.addressand stay inDiscoveredphase until the Service appears - This address is separate from
GRID_SWIM_BIND_ADDR: the SWIM gossip endpoint and the data-plane gateway address are distinct
The first-ingress discovery contract uses an explicit port override or the first declared Service port. It is appropriate for the local MetalLB environment. A production endpoint is represented and validated by host, named port, protocol, SNI, address scope, and generation; arbitrary or ambiguous advertised strings do not become routable endpoints.
Probe behavior: In Mutual mode, the GridSite controller performs a bounded
mTLS connection to spec.gatewayEndpoint.address. It verifies the configured CA,
serverName, and the canonical live-certificate pin. A successful probe reports
reason: TlsVerified. Connection failures move an Active site to Unreachable;
identity or trust failures move it to Connecting.
A SWIM-advertised certificate that does not match a configured pin is recorded
as reason: AdvertisedCertMismatch and does not change the phase: it arrives
over gossip and is not trust material. Trust comes from the handshake above,
where a pin mismatch on the live leaf is PinMismatch and still demotes.
Explicit Plaintext mode performs only a bounded TCP connection for
diagnostics. It never promotes a site to Active and is never selected as a
fallback when Mutual TLS configuration is incomplete or invalid.
GridSite Lifecycle Diagnostics
Use kubectl get gridsites to inspect current lifecycle phases:
kubectl get gridsites
Example output:
NAME PHASE NETWORK
op-e2e-sjd-net-grid-site-b Connecting op-e2e-sjd-net
To see the reason and diagnostic message:
kubectl get gridsite <name> -o jsonpath='{.status.phase}/{.status.reason}: {.status.message}'
Phase transitions and their cause
| From | To | Trigger |
|---|---|---|
| (new) | Pending | Resource created |
| Pending | Discovered | GridNetwork controller observes SWIM Alive member |
| Discovered | Connecting | GridSite controller: spec.gatewayEndpoint.address non-empty |
| Connecting | Active | GridSite controller: configured Mutual TLS identity probe succeeds |
| Active | Connecting | TLS identity or trust verification fails, or the endpoint is changed to plaintext |
| Active | Unreachable | Gateway address is missing, times out, or refuses the connection |
Security invariant: a SWIM peer must never become routable solely because it gossiped successfully. Discovery, authentication, and authorization are separate steps.
Troubleshooting
Phase stays Pending after SWIM convergence
- Check the
GridNetworkhas the labelgrid.praxis.fast/auto-discover-sites: "true". - Check that the
GridNetworkcontroller has SWIM running (GRID_SWIM_BIND_ADDRenv var set). - Check
kubectl get gridnetwork <name> -o jsonpath='{.status.connectedSites}'(must be > 0).
Phase stays Discovered (not advancing to Connecting)
- The site has no
spec.gatewayEndpoint.address. Verify the remote operator’sprovider-gatewayLoadBalancer Service exists and has an external IP assigned, or setGRID_GATEWAY_ADDRESSas an explicit override. The self-discovery poller will propagate the address through SWIM once discovered. - Reason will be
GatewayAddressMissing.
Phase stays Connecting
- Check
status.reason:TrustMaterialMissing: configure the CA Secret, local client identity,serverName, and canonical pin policy.TrustMaterialInvalid: trust material is malformed, oversized, or uses the deprecated PEM fingerprint field.UntrustedIssuer,IdentityMismatch,CertificateExpired, orCertificateNotYetValid: inspect the live gateway certificate and configured CA/server name.PinMismatch: the live leaf certificate does not match either configured canonical pin.AdvertisedCertMismatch: the gossiped certificate copy does not match a configured pin. Diagnostic, the site stays Active.HandshakeTimeoutorTlsProtocolError: the TCP endpoint answered but did not complete the expected TLS protocol.
Phase is Active, site became Unreachable
- The connection to
spec.gatewayEndpoint.addressfailed. When connectivity returns, the complete configured identity probe must pass before the site returns to Active.
RBAC for GridSite status updates
The GridSite and GridNetwork controllers both write to GridSite status.
The grid-operator-crd ClusterRole in deploy/operator/clusterrole-crd.yaml
includes gridsites/status with verbs get and patch.
Consumer Config
When GatewayRef.praxisConfig.generate: true, the AGN Operator applies a
ConfigMap in the gateway’s namespace on every reconcile. The
grid-operator-resources ClusterRole includes configmaps with verbs
create and patch. A RoleBinding in the gateway’s namespace is required
for the operator ServiceAccount to write the ConfigMap there.
Every clusterEndpoints[] entry must declare explicit transport intent via the
transport field. Remote/provider-gateway clusters should use
transport.mode: mutual_tls with a non-blank transport.sni matching the
provider certificate SAN. Local dev-only clusters may use
transport.mode: plaintext. Missing transport fails closed with status reason
MissingTransport; missing SNI on mutual_tls fails with MissingSni.
transport.mode is the security switch, not the presence of sni.
Credential Secret access
The generated ConfigMap references credential Secrets by name, namespace, and
key; it does not read Secret values. The operator does NOT require get access
to credential Secrets in the gateway namespace for config generation.
The final-hop gateway or provider-side component making the final backend call needs the credential Secret mounted. Secret provisioning in that cluster is the responsibility of external tooling (platform automation, External Secrets, Vault, or a manual process). The AGN Operator does not copy Secrets across clusters.
Cross-cluster limitations
The operator’s RBAC controls access within its own cluster. When the consumer
gateway runs in a different cluster, the generated ConfigMap must be delivered
externally: the operator cannot write to a remote cluster’s API server directly.
The Kind validation harness (verify-api-fallback-native) bridges this gap for
local testing by reading the generated YAML and re-applying it as
praxis-consumer-config in the consumer cluster. Production cross-cluster
delivery requires GitOps, External Secrets, or a similar mechanism.
Site Departure
The running SWIM implementation detects an abrupt loss through direct and
indirect probes. Remote provider records are treated as degraded when the peer
is Suspect or Dead, and stale-candidate retention follows the configured
overlay TTL policy.
The current operator does not delete the GridSite, garbage-collect the CRDT
record, or automatically complete a Left transition on process shutdown.
Departure therefore preserves control-plane evidence and requires explicit
site lifecycle cleanup by the deployment owner.
Adding a New Site to an Existing AI Grid Network
- Deploy the AGN Operator on the new cluster
- Create a
GridNetworkwith any existing cluster as a seed - SWIM discovers the existing cluster, which shares the membership list of all other sites
- The new site automatically discovers all grid members within seconds
- SWIM propagates public certificate material; the operator
verifies the explicitly configured fingerprint and advances matching sites
to
Active - Once
Active, the new site’s providers are visible to all other sites through the routing overlay
External Ingress Operations
External ingress operates as a two-stage service:
managed GTM -> Praxis AI edge -> AGN-selected Praxis provider gateway
The production deployment inventory contains:
| Layer | Operational inventory |
|---|---|
| GTM | Public service name, public TLS ownership, edge origins, health probes, steering policy, drain/failback policy, DDoS/WAF controls. |
| Edge | At least two failure domains, pinned AGN/AI/Praxis compatibility set, caller authentication, tenant/model authorization, request limits, accepted overlay status. |
| AGN | One edge-specific GatewayRef and routing perspective per edge location, authenticated site state, bounded admission policy, versioned overlay revisions. |
| Provider | Private backend, provider Praxis gateway, mTLS listener, trusted edge identities, local authorization, local limits, and final-hop credentials. |
Edge Readiness
A production GTM integration uses a route-aware readiness signal. The edge is ready when:
- its public listener and external authentication dependencies are healthy;
- a supported overlay version has been accepted;
- the accepted overlay age is within policy;
- endpoint/TLS configuration exists for every usable selected cluster; and
- the public offer has its configured minimum authorized route coverage.
One unavailable optional provider does not withdraw the edge. Loss of all required routes, a hard-expired snapshot, or a failed security dependency does. Liveness remains a process/listener signal and is not used as route coverage.
During drain, readiness is withdrawn before shutdown. New requests stop while existing SSE streams receive the configured completion interval.
Overlay Rollout Evidence
A production external-edge rollout tracks four distinct states:
desired revision
-> rendered ConfigMap revision
-> distributed file revision
-> Praxis accepted/serving revision
GridNetwork.status.praxisConfigStatus=Rendered reports successful desired
config rendering and apply. It does not report that the gateway loaded the
overlay. The production contract requires gateway status for the accepted
revision, digest, acceptance time, age, and last rejection reason. That
contract is not satisfied by compatibility profiles that expose only
Rendered status.
Operational checks compare the same revision across all four states and send a request whose internal route-decision record contains that revision.
An invalid overlay is expected to:
- fail strict parse or semantic validation;
- leave the previous accepted snapshot serving;
- increment a bounded rejection metric;
- expose the rejection reason without content or secrets; and
- affect readiness only according to accepted-snapshot age policy.
Provider Trust Verification
Control-plane GridSite Active is necessary but not sufficient. The data-plane
verification uses the actual edge-to-provider path:
known edge certificate -> accepted
unknown/revoked certificate -> rejected
wrong SNI/server identity -> rejected
direct public client -> rejected
authorized peer but denied provider policy -> rejected
The provider gateway’s backend remains ClusterIP-only or otherwise private.
Customer Authorization values are absent from the provider request. Provider
credentials exist only at the final hop.
Development Validation Environments
The xtask env commands provide a local development and
integration-validation path using Kind clusters. They are not the production
reconciliation model. The Kubernetes-native global-ingress scenario is
documented in
the Praxis demos repository.
This path is intended for:
- Local development iteration against a multi-cluster topology
- Integration validation before pushing to a real cluster
- CI pipelines that require a running kind environment
What xtask env does
xtask env commands are imperative and config-driven.
They operate from tests/env/config.toml (or a supplied
--config path), which declares clusters, their roles,
and the models each provider cluster exposes.
Available commands:
| Command | What it does |
|---|---|
cargo xtask env up | Creates kind clusters, deploys the configured provider backend, generates local test certificates |
cargo xtask env down | Tears down kind clusters and removes generated certs |
cargo xtask env status | Reports cluster, provider, and cert readiness |
cargo xtask env verify-providers | Probes Chat Completions endpoints against the configured provider backend in all provider clusters |
cargo xtask env build-gateway-images | Builds the Praxis AI gateway and mock EPP container images |
cargo xtask env load-gateway-images | Loads locally-built images into kind cluster nodes |
cargo xtask env deploy-provider-gateways | Applies generated Praxis AI gateway resources to provider clusters |
cargo xtask env verify-provider-gateways | Runs end-to-end probes through the provider gateway request path |
cargo xtask env deploy-consumer-gateway | Deploys a consumer Praxis AI gateway with a generated static intelligent_route config |
cargo xtask env deploy-consumer-gateway --overlay-config <path> | Deploys the consumer gateway using a routing-config.json routing overlay file |
cargo xtask env verify-gateway-e2e | Verifies consumer-to-provider routing end-to-end |
cargo xtask env verify-mtls-trust | Verifies provider gateway mTLS enforcement (positive + negative cases) |
cargo xtask env verify-api-fallback-native | Verifies native intelligent_route → credential_inject credential injection with token bytes absent from overlay and consumer ConfigMap |
cargo xtask env verify-stale-gc-ttl | Verifies GridNetwork.spec.staleCandidateTtlSeconds evicts stale remote candidates from the rendered overlay |
cargo xtask env verify-responses-routing | Verifies /v1/responses request parsing and AGN overlay routing using openai_responses_format -> intelligent_route filter chain |
cargo xtask env verify-crd-schema | Verifies required generated CRD schema fields without requiring kind clusters |
cargo xtask env verify-swim-dns-hostnames | Verifies hostname-based SWIM advertise/seeds, membership convergence, and CRDT provider-state propagation |
cargo xtask env verify-operator-install-rbac | Applies install manifests, runs positive/negative RBAC checks, proves minimal reconcile succeeds |
cargo xtask env validate-all | Runs the local validation suite and prints a Markdown result table |
Operator and SWIM local validation
The operator is not running inside kind; it connects to the kind cluster via the local kubeconfig. SWIM runtimes use localhost UDP sockets between local operator processes. This avoids requiring an operator container image or in-cluster RBAC for local validation.
Setup (one-time per machine)
cargo xtask env up -c tests/env/operator-routing.toml
cargo xtask env load-gateway-images -c tests/env/operator-routing.toml
Creates grid-site-a (provider, mock-openai backend)
and grid-consumer kind clusters, generates local mTLS
certificates, and loads Praxis AI gateway images.
CRD schema validation
cargo xtask env verify-crd-schema
This command runs the CRD generator and verifies the generated schema contains required AGN status and InferenceProvider routing and metrics fields. It does not require kind clusters.
Routing validation
cargo xtask env validate-operator-routing -c tests/env/operator-routing.toml
This command deploys the Praxis provider gateway, spawns
the operator out of cluster, applies GridNetwork and
InferenceProvider fixtures, waits for reconciliation,
exports the operator overlay, deploys the consumer
gateway from that overlay, and sends live HTTP requests
through the consumer gateway.
The validation covers provider health classification,
candidate ordering, metrics-aware ordering,
clusterName identity mapping, overlay export,
consumer gateway deployment, successful routing for a
known model, and clean failure for an unknown model.
SWIM membership
cargo xtask env verify-swim-membership -c tests/env/operator-routing.toml
This command starts two out-of-cluster operator
processes with distinct localhost UDP ports. The
secondary seeds on the primary. After a convergence
window, the command applies a GridNetwork fixture and
polls GridNetwork.status for SWIM-derived membership
state.
CRDT-over-SWIM state
cargo xtask env verify-swim-state -c tests/env/operator-routing.toml
To qualify hostname-based SWIM endpoints without relying on public DNS:
GRID_SWIM_DNS_EVIDENCE_DIR=/tmp/grid-swim-dns-run-1 \
cargo xtask env verify-swim-dns-hostnames -c tests/env/operator-routing.toml
The qualification uses two distinct loopback ports with localhost:port
advertise and seed endpoints, then proves membership and remote provider-state
propagation before cleaning its resources.
This command starts two SWIM-enabled operator processes,
waits for gossip convergence, then applies a
GridNetwork and an InferenceProvider. Each operator
maps the InferenceProvider CRD to a
crdt::ProviderState and publishes it as a
StateBroadcast over foca’s custom-broadcast path. The
receiver merges the GridStateSnapshot, and subsequent
status reconciliation reflects remote provider state in
GridNetwork.status.remoteProviderCount.
Provider fields propagated over SWIM:
| CRDT field | Source |
|---|---|
network_id | owning GridNetwork.metadata.name |
site_id | local SWIM site identity |
provider_id | metadata.name |
routing_cluster | spec.clusterName or metadata.name |
models | spec.models[*].name |
backend_kind | spec.backendKind |
phase | status.phase (including Unavailable) |
metrics | metricsConfig scrape results, or defaults |
revision | metadata.resourceVersion, falling back to metadata.generation |
writer_id | local SWIM site identity |
remoteProviderCount in GridNetworkStatus
reflects received remote provider records for the
current GridNetwork; local records and records from
other GridNetworks are excluded. The local validation
fixture expects exactly one remote provider record; zero
means state did not arrive, and more than one indicates
cross-network leakage or stale test state.
Three-node SWIM mesh
cargo xtask env verify-swim-mesh-three-node -c tests/env/operator-routing-multisite.toml
This command starts three SWIM-enabled operator processes in a linear topology: node A (no seeds), node B (seeds A), and node C (seeds B only, not A). It proves:
Transitive discovery: A learns about C through B. After gossip convergence,
GridNetwork.status.remoteProviderCount >= 2on A, confirming CRDT state from both B and C reached A transitively.Routing eligibility before Active: C’s CRDT provider is present in A’s SWIM state but absent from A’s routing overlay because C’s
GridSiteis not yetActive. Both B and C are excluded.Routing eligibility after Active: after C’s
GridSiteis set toActive(with a reachable egress address), A’s overlay is re-rendered and C’s provider candidate appears.Cross-network isolation: a wrong-network
GridNetworkandInferenceProviderare applied alongside the main network. The wrong-network model is confirmed absent from A’s correct-network overlay, proving providers cannot cross network scopes.
This validation proves that SWIM gossip alone is not sufficient for routing; explicit
Active phase assignment is required.
Full local validation suite
cargo xtask env validate-all -c tests/env/operator-routing.toml
This command runs the local status check, operator routing validation, SWIM membership validation, CRDT-over-SWIM state validation, and mTLS trust validation in sequence. It continues after individual step failures and prints a Markdown summary table at the end so CI logs and manual runs show the complete state of the environment.
Required local images
Before running load-gateway-images, the following
images must exist in the local container daemon:
| Image | Built from | Required for |
|---|---|---|
localhost/praxis-ai:llmd-ext-proc | AI repository external checkout | All provider and consumer gateways |
localhost/praxis-ai-mock-epp:latest | AI repository external checkout | All provider gateways |
grid-mock-providers:latest | This repository, crates/mock-providers/Containerfile | Provider clusters with backend = "mock-openai" only |
This table applies to the generic xtask env harness above
(validate-all, verify-swim-mesh-three-node,
verify-failover-under-lost-peer, etc.), which is the only
path that consumes these two locally-built defaults directly.
The named demos (grid-glb-demo, grid-combined-site,
grid-llmd-pool-metrics) do not need them; they override
GRID_XTASK_GATEWAY_IMAGE/GRID_XTASK_MOCK_EPP_IMAGE
(see crates/xtask/src/env/image_overrides.rs) with published
ghcr.io/praxis-proxy/ai images and never build
from an AI repository checkout.
As of this writing, neither Containerfile.composed nor a mock
llm-d Endpoint Picker server implementation exists in the AI
repository (tracked in
ai#716), so
build-gateway-images --ai-repo <path> cannot currently produce
either of the first two images. That gap blocks only the
generic-harness path (concretely, verify-failover-under-lost-peer
today), not any of the three named demos above. It is itself
soft-blocked on the in-flight, other-team-owned
ai#334
(ext_proc compatibility moving into the AI repository), which is
deliberately paused pending a release-timeline decision rather than
abandoned.
Use build-gateway-images --ai-repo <path> to build the first two images from
the AI repository source tree. Build grid-mock-providers:latest separately
from this repository:
docker build -t grid-mock-providers:latest -f crates/mock-providers/Containerfile .
What xtask env does NOT do
The xtask env up/down/status/deploy-* commands are
not the production operator:
- They do not reconcile Kubernetes resources continuously
- They do not manage
GridNetwork,GridSite, orInferenceProviderCRDs in a watch loop - They do not perform live config reload against a running gateway
The verify-swim-membership and verify-swim-state
commands do spawn out-of-cluster operator processes that
run real SWIM and CRDT reconciliation, but they use
localhost UDP sockets and ephemeral fixtures; they are
not a substitute for in-cluster production deployment.
In the production architecture, continuous reconciliation
is the responsibility of the AGN Operator and its
controllers. xtask env commands are a validation
convenience layer, not a production orchestrator.
Routing overlay file input
deploy-consumer-gateway --overlay-config <path>
accepts a routing-config.json routing overlay file. This
allows local validation of the overlay wire format and
consumer gateway config generation without running a
full production operator reconcile loop. The overlay
file format is:
{
"network": "<grid-network-name>",
"local_site": "<consumer-site-name>",
"candidates": [
{
"kind": "inference_model",
"name": "<model-name>",
"site": "<provider-site-name>",
"cluster": "<overlay-cluster-name>",
"fresh": true
}
]
}
When an overlay is supplied, intelligent_route.local_site
and candidates come from the overlay. The
load_balancer section is still generated from the
provider endpoints in the environment config.
Separation from production reconciliation
The production architecture is operator-driven. The AGN Operator reconciliation path owns long-lived management of:
GridNetwork,GridSite, andInferenceProviderCRD reconciliation- SWIM mesh formation and certificate lifecycle
- Routing overlay ConfigMap generation and application
xtask env is a development convenience layer that
uses the same config and cert infrastructure, not a
production orchestrator. Production reconciliation
semantics are defined by the AGN Operator controllers,
not by the imperative xtask env command flow.
Opinionated walkthroughs and topology fixtures
Scripts, static manifests, and walkthrough documentation for specific gateway-to-gateway topologies are maintained outside this repository in the accompanying research-spikes repository.
AGN keeps generic, config-driven, reusable commands. Topology-specific fixtures, static manifests, and presentation walkthroughs belong outside the AGN repository.
References
- HashiCorp memberlist: reference design for SWIM-style membership, gossip transport encryption, key rotation, and join/admission behavior. AGN uses foca rather than memberlist; foca used the Go-based memberlist implementation as a reference architecture, and AGN uses memberlist the same way: as an architectural reference for control-plane gossip hardening.