eks-agent-platform
The control plane
The CRDs a tenant is declared in — Platform, AgentFleet, ModelGateway, BudgetPolicy, EvalSuite — and the operator that reconciles them, including the per-tenant AWS identity and access it mints.
What follows is that repo's AGENTS.md — the entry point an agent reads before working on it. It is rendered from the repo itself at build time rather than copied, so it says what the repo says today. Links in it point back at the repo they were written for.
eks-agent-platform — agent entry point
You’re an AI client (or the author of one) about to declare a tenant on an EKS cluster, ship an agent fleet, set a budget, or configure a model gateway. This file gets you running in five minutes. For the wider picture — how this repo fits into the nanohype stack — read the Platform Reference.
What this repo gives you
A Kubernetes-native control plane that lets you declare agent platforms as CRDs and have an operator reconcile the AWS state, namespace boundary, tenant IAM identity, KMS grants, network policies, and runtime resources. Nine CRDs (version v1alpha1) split across three capability groups under the nanohype.dev domain — platform.nanohype.dev (Tenant, Platform), agents.nanohype.dev (AgentFleet, ModelGateway, AgentSandbox, SandboxPool), governance.nanohype.dev (BudgetPolicy, EvalSuite, SLOPolicy):
| CRD | What it owns |
|---|---|
Tenant |
Cluster-scoped aggregate of a team’s Platforms. Rolls up readiness, spend, and suspension state |
Platform |
Tenant Namespace, ResourceQuota, LimitRange, default-deny NetworkPolicy, ArgoCD AppProject, tenant-runtime ServiceAccount, per-Platform IAM role + Pod Identity association + KMS grant + S3 bucket policy, the Platform’s eval identity |
ModelGateway |
Envoy AI Gateway routes, Bedrock model ID resolution, Guardrails attachment, per-route rate limits |
AgentFleet |
Deployment per agent running the tenant’s own image, KEDA ScaledObject, per-fleet NetworkPolicy, all under the tenant ServiceAccount bound to the tenant IAM role via EKS Pod Identity |
SandboxPool |
Pull-based pool of always-on Managed Agents sandbox workers — a worker Deployment, default-deny NetworkPolicy, and a KEDA-autoscaled metrics bridge keyed on work-queue depth |
AgentSandbox |
Single-use hardened pod for one agent role-session — push-dispatched, Platform-gated, default-deny networked, garbage-collected after a TTL |
BudgetPolicy |
Hourly Athena rollup of CUR + CloudWatch in-flight estimate. Writes spend / percent / conditions to status. Publishes BudgetBreach to EventBridge at ≥120% |
EvalSuite |
Argo CronWorkflow per suite, run under the Platform’s eval identity. Gates Argo Rollouts via AnalysisTemplate on status.lastScore |
SLOPolicy |
Multi-window burn-rate evaluation of one objective against Amazon Managed Prometheus. Publishes BurnRateBreach to EventBridge, and holds the tenant’s ArgoCD auto-sync on a page-tier burn |
Plus:
operators/— Go operator binary registering nine reconcilers (one binary, one leader-election lease)charts/— Helm charts for installing the operator (CRDs + Deployment + RBAC + the eval-runtime bundle behindevalRuntime.*) + thetenantchart consumers use.eks-gitopsaddons-agent-operatorgit-sourcescharts/operatorand injects per-cluster IRSA to deliver it onto clustersexamples/— minimal end-to-end CR sets (Tenant + Platform + ModelGateway + AgentFleet + BudgetPolicy) you can copy
Contract surface
The Platform CR is the entry point. Minimum shape (full field reference in docs/crd-reference/v1alpha1.md):
apiVersion: platform.nanohype.dev/v1alpha1kind: Platformmetadata: name: my-app namespace: eks-agent-platformspec: displayName: 'My App' persona: ops # sales-ops | support | finance | ops | founder | eng | marketing | legal | generic tenant: my-team budget: name: my-app # must reference an existing BudgetPolicy CR in the same namespace identity: allowedModelFamilies: [anthropic] # Bedrock families the operator-reconciled tenant role grants invoke on extraPolicyArns: [] # managed IAM policy ARNs to attach on top of the baseline compliance: soc2: true isolation: namespace # namespace (default) | vclusterA BudgetPolicy CR is required (Platform.spec.budget.name references it):
apiVersion: governance.nanohype.dev/v1alpha1kind: BudgetPolicymetadata: name: my-app namespace: eks-agent-platformspec: platformRef: { name: my-app } monthlyUsd: '2500' alertThresholdsPercent: [50, 80, 100] killSwitchEnabled: true # at 120% the operator detaches the baseline IAM policyTenant workload identity
A Platform’s pods reach AWS through one IAM role, <cluster>-<platform>-tenant, and one ServiceAccount, tenant-runtime. The operator creates all three pieces of that binding:
- the tenant role, minted under the tenant IAM path with the configured tenant permissions boundary, trusting only
pods.eks.amazonaws.com; - the
tenant-runtimeServiceAccount in the workload namespacetenants-<platform>, carrying no role-arn annotation. Underspec.isolation: vclusterthe operator creates it inside the virtual cluster instead, and vcluster’s syncer copies it to the host under a translated name; - the EKS Pod Identity association binding that host ServiceAccount to the tenant role.
The operator binds no other ServiceAccount to the tenant role, so a pod running under one gets no tenant credentials. Every workload that needs the tenant’s AWS identity runs under tenant-runtime, and the platform-tenant contract holds a tenant chart to the same binding:
| Workload | How it runs under tenant-runtime |
|---|---|
The application’s chart pods (<app>/chart/) |
The chart references the operator’s ServiceAccount (serviceAccount.create: false, serviceAccount.name: tenant-runtime) and creates none of its own (contract rule no-chart-serviceaccount) |
| AgentFleet agent pods and AgentSandbox session pods | The operator sets serviceAccountName: tenant-runtime on the PodSpec it builds |
| The ModelGateway’s Envoy proxy | The operator pins the EnvoyProxy’s envoyServiceAccount to tenant-runtime, so the gateway invokes Bedrock as the tenant |
No ServiceAccount a tenant chart renders or references carries an eks.amazonaws.com/role-arn annotation (contract rule no-role-arn-annotation): the annotation adds IRSA web-identity credentials to the pod, and the tenant role trusts only the Pod Identity service, never sts:AssumeRoleWithWebIdentity.
The tenant role carries the baseline Bedrock policy + spec.identity.extraPolicyArns, plus the inline policies the operator reconciles — model scoping, datastores, capabilities and direct secret reads from the Platform’s declarations, and use of the tenant’s own KMS key, whose ARN tenant-substrate publishes. Its name is cluster-keyed (not env-keyed) so two clusters can host a Platform of the same name without their roles colliding. The operator mints two further roles that no ServiceAccount is bound to: <cluster>-<platform>-session when spec.attribution is set, which only the tenant role may assume, and <cluster>-<platform>-scheduler-invoke when the eventBridgeScheduler capability is declared, which the tenant role passes to EventBridge Scheduler.
The association the operator created is reported on Platform.status.podIdentity — clusterName, namespace, serviceAccount, roleArn. Read it rather than deriving the binding: the trust policy carries no subject (under Pod Identity the (namespace, service-account) constraint lives in the association, so every tenant role’s trust document is byte-identical), and the bound ServiceAccount is not always tenant-runtime — under spec.isolation: vcluster it is the vcluster-translated host name. Anything auditing the binding, in this repo or another, reads that field.
A Platform’s eval runs hold a separate identity, not tenant-runtime, and status.podIdentity does not report it. The operator mints the <cluster>-<platform>-eval role, recorded on status.evalRoleArn, and the eval-<platform> ServiceAccount in the eval runtime namespace (eval-runner by default), recorded on status.evalNamespace, and associates the two; the EvalSuite reconciler submits every run under that account. The role’s one inline policy reaches only the Platform’s own prefixes in the shared eval-reports bucket — read on <platform>/manifests/*, write on <platform>/runs/* — and carries no Bedrock: the runner calls the Platform’s ModelGateway. Tenants deploy nothing into the eval runtime namespace, so no tenant workload can run under it. ARCHITECTURE.md has the full model.
Declare a tenant
- Apply a
BudgetPolicyCR in the tenant namespace (or alongside Platform — operator handles ordering). - Apply a
PlatformCR referencing the BudgetPolicy. - The operator reconciles:
- Namespace
tenants-<platform>(withpod-security.kubernetes.io/enforce: restrictedlabel) ResourceQuota+LimitRangedefaults- Default-deny
NetworkPolicyplus egress allow-list (DNS, the model gateway, OTel collector) - ArgoCD
AppProjectscoped to the tenant namespace tenant-runtimeServiceAccount, the one the app chart and the operator-built pods run under- IAM role
<cluster>-<platform>-tenantbound to thetenant-runtimeSA via an EKS Pod Identity association; attaches baseline Bedrock policy + everything inspec.identity.extraPolicyArns, and reconciles abedrock-model-scopinginline policy that limits Bedrock invoke to the ARNsspec.identity.allowedModelFamilies/allowedModelsexpand to (both unset = all model invoke denied)
- Namespace
- Status reaches
Ready; the app’s ApplicationSet entry can start syncing.
Ship an agent fleet
- Confirm the tenant Platform is
Ready. - Apply a
ModelGatewayCR (optional but recommended) declaring the model routes the agents will hit. - Apply an
AgentFleetCR referencing the Platform. The operator reconciles a Deployment per agent — the tenant’s own image, under the tenant ServiceAccount — plus the KEDA scaler. - Fleet pods run as the
tenant-runtimeSA; the Pod Identity association the operator created vends the tenant IAM role’s credentials to them.
Kill-switch
When a BudgetPolicy hits 120% of monthlyUsd and killSwitchEnabled: true, an EventBridge rule → Step Functions state machine:
- Detaches the Bedrock-invoke baseline policy from the tenant’s IAM role
- Tags the role with
platform.nanohype.dev/suspended=true - The
PlatformReconcilerobserves the tag and stops re-attaching the baseline on subsequent reconciles - Status moves to
Suspendedwith aSuspendedcondition
Recovery is human-only — an operator clears the suspension tag manually after the breach is resolved. The reconciler does not auto-restore.
Conventions
- Conventional Commits enforced via
commitlint.config.mjs(scope enum:operators,charts,terraform,core,sdk,pricing,client,cli,examples,docs,ci,release,deps,security) - Go:
go fmt,go vet,golangci-linton PR - Tests:
go test ./internal/...; in-memory fakes for AWS clients (seeoperators/internal/controller/platform_iam_reconcile_test.gofor the pattern) - Generated artifacts (CRD manifests, deepcopy code) committed;
make manifestsregenerates them - CRD API groups are org-aligned under the
nanohype.devdomain:platform.nanohype.dev(Tenant, Platform),agents.nanohype.dev(AgentFleet, ModelGateway, AgentSandbox, SandboxPool),governance.nanohype.dev(BudgetPolicy, EvalSuite, SLOPolicy). Finalizers, label/tag keys, and the leader-election lease ID follow the same domain. A Platform’s tenant field carries the owning team’s real name —strategy,growth,reliability,workplace
Pointers
ARCHITECTURE.md— bounded contexts, data flow, load-bearing decisions, the operator-owned-vs-tofu-owned splitdocs/crd-reference/v1alpha1.md— field-by-field CRD reference (generated from godoc)examples/— end-to-end CR sets you can copyREADME.md— install, run, contribute- Platform Reference — the stack-wide view
landing-zone/AGENTS.md— itsagent-iamcomponent provisions the operator’s own role, the tenant permissions boundary, and the IAM path every tenant role is minted under