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 things you must NOT do inside a chart.

Nameplatform-tenant-contract
Version1
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

    ServiceAccount used by the pod. It carries no role-arn annotation: the eks-agent-platform operator creates an EKS Pod Identity association binding the tenant ServiceAccount to the per-Platform IAM role, whose scoped datastore-access policy the operator generates from Platform.spec.datastores. `serviceAccount.name` must match the tenant-runtime ServiceAccount the association targets.

  • 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) 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: that provisioning is not yet wired end to end, so declare the stores but do not assume they exist at deploy time. 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. NOT YET WIRED END TO END, and build accordingly: the module and the IAM generation both exist, but nothing yet carries a Platform CR's spec.datastores into the module's var.tenants input, which is empty in development, staging and production. Declaring datastores is correct and forward-compatible and remains what you should do — but no declared store is provisioned in any environment today, so an app must not assume its database, bucket or queue exists at deploy time. 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 prefix plus iam:PassRole (Scheduler-service-capped) on an operator-minted <env>-<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.
do_not
  • Do NOT scaffold IAM roles inside the chart, and do NOT annotate the ServiceAccount with a role ARN. The eks-agent-platform operator provisions the per-Platform IAM role — with a datastore-access policy generated from spec.datastores — and creates the EKS Pod Identity association that binds the tenant ServiceAccount to it. The chart carries no role ARN.
  • Do NOT hand-write a per-app landing-zone component for the tenant's databases, buckets, queues, caches, or streams. Declare them in Platform.spec.datastores — 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) still land in `nanohype/landing-zone` or `nanohype/eks-gitops`, never in-app tofu.
  • Do NOT reference a hand-written managed policy through extraPolicyArns for SES or EventBridge Scheduler. Declare them in Platform.spec.identity.capabilities — the operator generates the scoped grants (and mints the scheduler-invoke role). extraPolicyArns stays the escape hatch only for grants outside both the datastore and capability vocabularies.
  • Do NOT rely on a broad Secrets Manager grant for the tenant role. Secrets projected into the pod by the chart's ExternalSecret need no grant at all (the External Secrets controller reads them under its own identity); for the few a pod reads itself via the SDK, list them in Platform.spec.identity.directSecretReads so the operator grants read on exactly those.
  • Do NOT add cluster-level addons in the chart (ingress controller, cert-manager, External Secrets, observability stack). Those are gitops-repo concerns in `nanohype/eks-gitops`.
  • Do NOT skip per-env `values-{dev,staging,production}.yaml` — every chart has three deltas even if some are empty.
  • Do NOT hardcode AWS account IDs, region names, or KMS key ARNs in chart values. Per-env values plumb them in from landing-zone outputs at deploy time.