Skip to content

Platform tenant contract

Every k8s-native deliverable on the nanohype stack ships as a Platform tenant — a self-contained unit the eks-agent-platform operator can scaffold, suspend (via kill-switch), and tear down via CR reconciliation. This contract names the artifacts a chart must produce, the minimum Platform CR shape, the OTel resource attributes the operator expects, and the rules a chart follows, each with its severity.

Nameplatform-tenant-contract
Version2
Sourcestandards/platform-tenant-contract.json

The standard

required_artifacts
  • path
    <app>/chart/Chart.yaml
    description
    Helm chart manifest.
  • path
    <app>/chart/values.yaml
    description
    Base values shared across all environments.
  • path
    <app>/chart/values-{dev,staging,production}.yaml
    description

    Per-env delta files. Required even if empty — tooling consistency depends on the presence of all three.

  • path
    <app>/chart/templates/deployment.yaml (or statefulset.yaml)
    description
    The workload itself. Stateful workloads use StatefulSet; everything else uses Deployment.
  • path
    <app>/chart/templates/service.yaml
    description
    ClusterIP Service exposing the workload to in-cluster consumers.
  • path
    <app>/chart/templates/serviceaccount.yaml
    description

    References the operator-owned tenant-runtime ServiceAccount (`serviceAccount.create: false`, `serviceAccount.name: tenant-runtime`) rather than creating one. The eks-agent-platform operator creates that ServiceAccount in the workload namespace and an EKS Pod Identity association binding it to the per-Platform IAM role, whose scoped datastore-access policy the operator generates from Platform.spec.datastores. It carries no role-arn annotation.

  • path
    <app>/chart/templates/networkpolicy.yaml
    description
    Default-deny NetworkPolicy plus explicit egress allow-list.
  • path
    <app>/gitops/applicationset-entry.yaml
    description
    ApplicationSet entry to be registered with `nanohype/eks-gitops`.
  • path
    <app>/platform.yaml
    description

    Platform CR (and any required BudgetPolicy CR, plus the ModelGateway CR whose routes an app that calls a model names) declaring the tenant boundary AND its stateful substrate (spec.datastores). The CR objects live in the team namespace (metadata.namespace: tenants-<team>); the eks-agent-platform operator provisions a per-Platform WORKLOAD namespace named tenants-<platform-name> — where the app's chart deploys — plus its ResourceQuota, LimitRange, default-deny NetworkPolicy, the ArgoCD AppProject named <platform-name>, and the per-Platform IAM role with a datastore-access policy generated from spec.datastores. The declared datastores themselves are provisioned by the generic tenant-substrate module from that same declaration — there is no per-app substrate component. See datastore_vocabulary: landing-zone renders tenant-substrate's var.tenants from the Platform CRs each environment's tenants.selection.yaml names, so a declared store is provisioned in an environment only once its tenant is selected there. The app's ApplicationSet entry must target that per-Platform namespace + project.

  • path
    <app>/agentfleet.yaml (AI workloads only)
    description

    Optional AgentFleet CR declaring one or more agents, each an image bound to a named ModelGateway route, which the operator runs as a Deployment behind a KEDA scaler. Required only when the app runs an AI agent fleet inside its tenant.

platform_cr_shape
apiVersion
platform.nanohype.dev/v1alpha1
kind
Platform
metadata
name
<app-name>
namespace
tenants-<team>
spec
displayName
<human-readable>
persona

<one of: sales-ops | support | finance | ops | founder | eng | marketing | legal | generic>

tenant
<team>
budget
name
<BudgetPolicy CR name in same namespace>
identity
allowedModelFamilies
  • anthropic
extraPolicyArns
—
capabilities
—
directSecretReads
—
compliance
soc2
true
hipaa
false
isolation
namespace
datastores
  • name
    <short-name>
    kind
    <one of: relational | keyValue | objectStore | queue | cache | stream>
    deletionPolicy
    Retain
datastore_vocabulary
summary

Declare the tenant's stateful stores in spec.datastores instead of hand-writing a landing-zone component. Each entry names a datastore and its kind; the generic tenant-substrate module provisions the resource and the operator generates the scoped IAM to reach it, so tenant count is unbounded. landing-zone renders each environment's tenant-substrate var.tenants from the Platform CRs that environment's tenants.selection.yaml names (scripts/render-tenants.py; CI fails on drift), so a declared store is provisioned in an environment only once its tenant is selected there. Declaring datastores is correct whether or not an environment selects the tenant, and an app must not assume its database, bucket or queue exists at deploy time in an environment that does not. Each kind carries at most the one typed config block matching it — omit it to take the young/light defaults. keyValue requires its block (a DynamoDB table has no default partition key); stream carries none.

kinds
relational

Aurora PostgreSQL Serverless v2. Config: engineVersion, minACU/maxACU (string, 0.5-ACU steps), backupRetentionDays, deletionProtection.

keyValue

DynamoDB. Config (required): partitionKey {name,type}; optional sortKey, billingMode, ttlAttribute, pointInTimeRecovery, globalSecondaryIndexes.

objectStore
S3. Config: versioning, lifecycleExpireDays.
queue

SQS with an optional dead-letter queue. Config: fifo, visibilityTimeoutSeconds, messageRetentionSeconds, maxReceiveCount (>0 provisions the DLQ).

cache
ElastiCache (Valkey/Redis). Config: engine, nodeType, replicas.
stream
MSK Serverless with IAM auth. No config block.
deletion

deletionPolicy defaults to Retain: deleting the Platform CR orphans the datastore intact (it keeps the Tenant tag recording what owned it), and the operator holds no delete permission on any datastore. The per-kind deletion_protection backstop is a second gate, defaulting closed.

capability_vocabulary
summary

Managed AWS capabilities outside the datastore vocabulary — SES send, EventBridge Scheduler — are declared in spec.identity.capabilities. The operator generates the scoped IAM from that declaration (a capability-access policy on the tenant role, and for eventBridgeScheduler a minted scheduler-invoke role), so these ride the declaration too instead of a hand-written managed policy referenced by ARN.

capabilities
ses

Grants ses:SendEmail / ses:SendRawEmail scoped by a ses:FromAddress condition to the tenant's sending domain, plus ses:GetSendQuota. The verified sending identity is account-level mail infrastructure in landing-zone, not provisioned per app.

eventBridgeScheduler

Grants scheduler:*Schedule on the tenant's own schedule group, <env>-<platform>, which the operator creates, plus iam:PassRole (Scheduler-service-capped) on an operator-minted <cluster>-<platform>-scheduler-invoke role, which may SendMessage to the tenant's own queue datastores. Declare a queue datastore as the schedule's target.

secret_access
summary

Most secret material reaches a tenant through the chart's ExternalSecret, projected by the cluster's External Secrets controller under its own identity — the tenant role needs no Secrets Manager grant for those. Only the secrets a pod resolves itself through the pod role via the AWS SDK are declared in spec.identity.directSecretReads, and the operator grants secretsmanager:GetSecretValue / DescribeSecret on exactly those.

directSecretReads

A list of secret names under the tenant's own <platform>/<env>/ prefix (e.g. "grafana/oncall-webhook-hmac"). Use it for the values a pod reads directly rather than by ExternalSecret projection — rotation-sensitive secrets a handler re-fetches and caches by version, or config bulk-loaded at startup. Each entry scopes to arn:...:secret:<platform>/<env>/<name>-*; leaving it empty means the tenant role holds no Secrets Manager grant at all.

otel_resource_attrs
  • name
    agents.tenant
    required
    true
    description
    The owning team. Matches Platform.spec.tenant.
  • name
    agents.platform
    required
    true
    description
    The Platform CR name. Used by dashboards to slice by app.
  • name
    agents.model_family
    required
    false
    description

    AI workloads only. One of: anthropic, amazon, meta, mistral, etc. Drives cost attribution and guardrail routing.

  • name
    agents.model_id
    required
    false
    description
    AI workloads only. The full Bedrock model ID being invoked.
rules
  • id
    no-chart-iam-role
    summary

    The chart scaffolds no IAM role or policy. The eks-agent-platform operator provisions the per-Platform IAM role, with a datastore-access policy generated from spec.datastores; a role the chart creates is one no Pod Identity association binds.

    severity
    reject
  • id
    no-chart-serviceaccount
    summary

    The chart creates no ServiceAccount of its own. It references the operator-owned tenant-runtime ServiceAccount (serviceAccount.create: false, name: tenant-runtime), which is the one the operator's Pod Identity association targets; a chart-created ServiceAccount gets no association and its pods get no AWS credentials.

    severity
    reject
  • id
    no-role-arn-annotation
    summary

    No ServiceAccount the chart renders or references carries an eks.amazonaws.com/role-arn annotation. Identity is EKS Pod Identity, bound by the operator; the annotation adds IRSA web-identity credentials to the pod, and the tenant role trusts only pods.eks.amazonaws.com, never sts:AssumeRoleWithWebIdentity.

    severity
    reject
  • id
    declare-datastores
    summary

    The tenant's databases, buckets, queues, caches and streams are declared in Platform.spec.datastores, never in a hand-written per-app landing-zone component. The generic tenant-substrate module provisions them and the operator generates the scoped IAM. Cloud-substrate gaps the datastore vocabulary does not cover (a shared VPC, base IAM, a KMS key, a new addon) land in `nanohype/landing-zone` or `nanohype/eks-gitops`, never in-app tofu.

    severity
    warn
  • id
    declare-capabilities
    summary

    SES send and EventBridge Scheduler are declared in Platform.spec.identity.capabilities, never granted by a hand-written managed policy referenced through extraPolicyArns. The operator generates the scoped grants and mints the scheduler-invoke role. extraPolicyArns is the escape hatch only for grants outside both the datastore and capability vocabularies.

    severity
    warn
  • id
    scoped-secret-reads
    summary

    The tenant role holds no broad Secrets Manager grant. Secrets the chart's ExternalSecret projects need no grant (the External Secrets controller reads them under its own identity); the few a pod reads itself through the SDK are listed in Platform.spec.identity.directSecretReads, and the operator grants read on exactly those.

    severity
    warn
  • id
    no-cluster-addons
    summary

    The chart adds no cluster-level addon (ingress controller, cert-manager, External Secrets, observability stack). Addons are gitops-repo concerns in `nanohype/eks-gitops`.

    severity
    warn
  • id
    per-env-values
    summary

    Every chart carries all three per-env deltas, `values-{dev,staging,production}.yaml`, even when some are empty.

    severity
    warn
  • id
    no-hardcoded-account-values
    summary

    Chart values hardcode no AWS account ID, region name or KMS key ARN. Per-env values plumb them in from landing-zone outputs at deploy time.

    severity
    warn