Skip to content

App CRD

Complete field reference for the App custom resource (tendtech.io/v1alpha1) — the single source of truth for one Tend-governed application. Read this when you need the exact type, validation, default, or writer of any spec or status field. For the reasoning behind the model see the governance model and app lifecycle; for creating one see onboard a new app.

Source of truth: operator/api/v1alpha1/app_types.go.

Resource identity

Property Value
CRD name apps.tendtech.io
API group / version tendtech.io/v1alpha1
Kind App
Scope Namespaced
Short name app
Status subresource Yes (/status)
Printer columns STATE (.status.state), OWNER (.spec.owner), EXPIRY (.spec.expiry), AIACT (.spec.purpose.decisionsAboutPeople)

spec is user-written (the owner and platform tooling); status is written only by the Tend operator. The build pipeline and promotion wiring report into the CR via annotations (below), never by writing status directly.

Spec fields

Field Type Required Default Validation / meaning
owner string yes creating identity, at admission MinLength=1. The Keycloak identity responsible for this App. No App exists unowned; deactivation past the grace period freezes the app (see app lifecycle).
expiry timestamp (metav1.Time) yes now + platform TTL, at admission When the App freezes if not renewed. Renew via the tendtech.io/renew annotation or by editing this field directly — see app lifecycle.
dataScopes []DataScope no (defaulted) [{name: synthetic, sourceType: synthetic, access: readwrite}] Declared, named, typed, access-scoped sources reachable through the data gateway. List type map keyed on name — names are unique, schema-enforced.
resourceCaps ResourceCaps no (defaulted) platform policy ("1" / "1Gi") Per-app compute ceilings. The operator renders them into a per-namespace ResourceQuota/LimitRange; Kyverno validates workloads against them.
costCap CostCap no (defaulted) platform policy ("50.00" EUR monthly) Per-app spend ceiling driving cost warnings and suspension.
purpose PurposeClassification no — required at promotion never defaulted What the App is for. The promotion path refuses submission without it (422 gate).
repoRef RepoRef no The per-app Gitea repository and its tracks. Populated by the platform at onboarding (001 contract: operator-populated), read by the promotion path.
builder string no The integrated builder/adapter that produced the App.
tier enum tier1 | tier2 no tier1 (CRD default) The inference boundary the App's builder runs against. Tier 2 is reserved and not exercised this slice. See boundary tiers.

No network, ingress, or egress declaration fields exist on the spec. The network perimeter is derived from dataScopes by the operator, never declared; a manifest carrying such fields fails schema validation as unknown fields under structural pruning. The rendering decision is ADR-0007.

DataScope

Field Type Required Validation
name string yes MinLength=1; unique within the list. Addresses the source at the data gateway as /v1/sources/{name}.
sourceType enum yes synthetic | postgres | s3. The v1 connector baseline; REST and warehouse connectors are pre-v1 follow-ups that extend this enum.
access enum yes read | readwrite. Evaluated per request by the data gateway; readwrite permits writes but never DDL.

See data gateway API for how scopes are enforced and declare and change data scopes for the workflow.

ResourceCaps

Field Type Meaning
cpu quantity string CPU ceiling, e.g. "1". Must parse as a resource.Quantity.
memory quantity string Memory ceiling, e.g. "1Gi".

CostCap

Field Type Meaning
amount decimal string The cap, e.g. "50.00".
currency string ISO 4217 code, e.g. EUR.
period enum monthly — the only supported accrual window this phase.

PurposeClassification

Field Type Meaning
category string Free-form purpose label, e.g. internal-tool.
decisionsAboutPeople bool Drives the EU AI Act triage marker. Tend surfaces the marker; it never adjudicates conformance.

RepoRef

Field Type Meaning
url string URL of the per-app git repository (Gitea).
sandboxBranch string The mutable iteration track. The promotion path treats an empty value as sandbox.
productionBranch string The promoted, approval-gated track. Empty is treated as production.

Admission defaulting

The Kyverno mutate ClusterPolicy app-field-defaults (deploy/policies/kyverno/app-field-defaults.yaml) fills missing governance fields at App CREATE. Mutating webhooks run before validating webhooks, so a bare App is defaulted and then passes the app-required-fields validation backstop in the same admission. Defaults are written onto the CR (explicit over implicit): what admission defaulted is what the estate shows and what promotion snapshots. Values come from the GitOps-delivered ConfigMap policy-defaults in namespace kyverno.

Field Defaulted to Source
owner the creating identity (request.userInfo.username) admission request context
expiry now + defaultTTL (2160h = 90 days) policy-defaults ConfigMap
dataScopes [{name: synthetic, sourceType: synthetic, access: readwrite}] policy (inline)
resourceCaps defaultCpuCap / defaultMemoryCap ("1" / "1Gi") policy-defaults ConfigMap
costCap defaultCostAmount / defaultCostCurrency / defaultCostPeriod ("50.00" / EUR / monthly) policy-defaults ConfigMap
purpose never defaulted — optional until promotion, strict at the promotion 422 gate

Each rule uses Kyverno's +(field) add-if-absent anchor: an explicitly set field is never overwritten. Changing a default is a ConfigMap edit that Argo CD reconciles — no policy change. The admission engine decision is ADR-0006.

Status fields

Only the operator writes status.

Field Type Meaning
state enum Lifecycle state: sandbox | promotion-pending | production | frozen | retired. frozen/retired are live (005-lifecycle-estate) — see app lifecycle for causes, retention, and restore.
lifecycle LifecycleStatus Operator-owned lifecycle posture: ownerState, orgUnit, freeze (causes + since), priorState, lastRenewedAt/retiredAt/restoredAt, archiveRef, revokedScopes, and the durable history of every lifecycle transition. See app lifecycle and estate portal & revoked scopes.
namespace string The provisioned per-app workload namespace (tend-app-<name>).
sandboxArtifact ArtifactRef Latest attested sandbox image + evidence.
productionArtifact ArtifactRef Set on a successful promotion.
attestationRefs []ArtifactRef History of attested artifacts, one per iteration.
promotion PromotionStatus The current/last promotion. The PromotionRequest entity lives inline here — there is no separate promotion store.
costStatus CostStatus Operator-owned cost posture (accrued, state, thresholds).
suspension Suspension Present if and only if the App is currently suspended — by the cost ladder or by a manual estate suspend; cleared on resume.
conditions []metav1.Condition See the conditions table below.
lastError string Most recent build/inference failure surfaced to the owner.
observedGeneration int64 Most recent generation reconciled.

ArtifactRef

Field Type Meaning
image string Harbor image reference, digest-pinned.
attestationDigest string OCI digest of the co-located attestation.
promptHash string Hash of the prompt that produced this artifact.
model string Model identity + version that produced it.
builtAt timestamp When the artifact was built.

PromotionStatus

Field Type Meaning
prUrl string The platform-raised promotion PR (sandbox → production).
snapshotRef string The sandbox commit the PR was opened from; fixed at submit so sandbox iteration cannot move the PR.
declared PromotionDeclared Required-field snapshot captured at submit: dataScopes (structured, shape parity with spec), owner, expiry, purpose.
evidenceRef string The evidence record attached to the PR (committed as evidence.json).
approverGroup string The Keycloak platform-approver group assigned as reviewers (default platform-approvers).
decision enum pending | approved | rejected.
approver string Recorded on approval.
decidedAt timestamp When the decision was recorded.
reason string Recorded on rejection.

CostStatus

Field Type Meaning
accrued decimal string Period-to-date attributed cost (OpenCost allocation).
currency string Mirrors the cap currency.
state enum normal | warning-75 | warning-90 | suspended | unknown. unknown means cost telemetry is missing and is never enforced on — an app is not punished for a telemetry gap.
lastEvaluated timestamp Last successful cost poll.
lastTransition timestamp When state last changed.
warning75At timestamp Set once per period-crossing of the 75% threshold (notify once per crossing, oscillation-safe); cleared at period rollover.
warning90At timestamp The 90% counterpart of warning75At.
lastSuspendedAt timestamp Durable trace of the last cost suspension. Persists after resume; never cleared at period rollover — an audit trail, not a per-period guard.
lastResumedAt timestamp Set when a suspension is lifted (cap raise or resume annotation).

Cost enforcement is deliberately not a status.state value: a production app suspended for cost is still state: production with the Suspended condition true and costStatus.state: suspended. See respond to a cost-cap suspension.

Suspension

Field Type Meaning
reason string Why enforcement fired; CostCapBreached (the cost ladder) or ManualSuspend (a platform engineer's estate suspend action). The two are independent — the cost reconciler never clears a manual suspension.
at timestamp When enforcement fired.
resumableBy string Documents the resume contract: owner raises spec.costCap.amount above accrued cost, or applies the tendtech.io/resume annotation.

Conditions

Condition True when Notes
Ready The App's workload namespace is provisioned.
Isolated The sandbox isolation floor (default-deny networking, gateway-only perimeter, no route to production) is provisioned in the workload namespace. Set false (not fatal) when the network-policy CRD is absent, so reconciliation still progresses.
BuildSucceeded An attested sandbox artifact has been recorded. False after a build/inference failure; the App stays in sandbox and status.lastError carries the error.
Suspended The App is suspended — reason CostCapBreached (cost ladder) or ManualSuspend (estate suspend action). Orthogonal to status.state; cleared on resume.
AIActTriage The App is classified as making decisions about people. Surfaced, never adjudicated.
Frozen The App is lifecycle-frozen. Causes compose; the reason lists the active ones, and the App unfreezes only when every cause clears.
Ownerless The owner is confirmed deactivated at the identity provider. Drives the freeze grace period.
ScopeRevoked A declared scope is revoked platform-wide. Informational — never freezes and never suspends; the runtime denial is the data gateway's.
OwnerSyncDegraded Owner state could not be resolved. Never enforced on.

Mutability

  • spec.dataScopes, spec.resourceCaps, spec.costCap are owner-mutable at any time. Scope changes reconcile into enforced behavior without redeploy (target ≤5 minutes, SC-004).
  • Raising spec.costCap.amount above accrued cost is one of the two resume paths. Lowering it below accrued triggers breach evaluation at the next cost reconcile.
  • The tendtech.io/resume: "true" annotation is the explicit resume path. The operator consumes (removes) it, so a stale annotation cannot pin an app un-suspendable across future breaches. The resume is recorded in evidence.
  • status.* is operator-only. Users, the builder, and the pipeline never write status; they report via the annotation contracts below.

Operator-owned labels, annotations, and finalizer

On the per-app workload namespace

Key Kind Meaning
tendtech.io/app label The owning App's name.
tendtech.io/owner label Label-safe rendering of spec.owner.
tendtech.io/env label Environment of the namespace; sandbox at provisioning.
tendtech.io/app-ref annotation Back-reference (namespace/name) to the App CR — a cross-scope ownerReference is not permitted, so the link is an annotation.

On the App CR — build report (written by the build pipeline)

The narrow inbound contract by which Tekton/Chains reports an attested sandbox artifact. The operator reconciles these into status.

Key Meaning
tendtech.io/sandbox-image Digest-pinned sandbox image.
tendtech.io/attestation-digest OCI digest of the attestation.
tendtech.io/prompt-hash Prompt hash for provenance.
tendtech.io/model Model identity + version.
tendtech.io/built-at Build timestamp.
tendtech.io/build-error Set instead of the above on a failed build.

On the App CR — promotion decision feedback

The inbound contract by which a Gitea PR merge/close is reported back; the operator reconciles these into status.promotion.

Key Meaning
tendtech.io/promotion-decision approved | rejected.
tendtech.io/promotion-approver Approver identity (on approved).
tendtech.io/promotion-reason Reason (on rejected).
tendtech.io/promotion-decided-at RFC3339 decision time (optional).
tendtech.io/production-image Digest-pinned production image (on approved).

On the App CR — owner-applied

Key Meaning
tendtech.io/resume "true" requests resume from cost suspension; consumed by the operator.

On operator-generated objects

Key Kind Meaning
tendtech.io/generated-by label tend-operator on every per-app object the operator generates (Cilium policies, gateway discovery ConfigMap, ResourceQuota/LimitRange). The provenance check asserts every per-app policy object carries it — never hand-written.
tendtech.io/policy label Policy-specific marker on generated Cilium policies.

Finalizer

Key Meaning
tendtech.io/cleanup Guards workload-namespace cleanup: deleting an App first releases its per-app namespace (and everything that cascades with it) before the App record is removed.

Example manifest

apiVersion: tendtech.io/v1alpha1
kind: App
metadata:
  name: expense-helper
  namespace: tend-system
spec:
  owner: u:alice@customer.example
  expiry: "2026-10-08T00:00:00Z"
  dataScopes:
    - name: synthetic
      sourceType: synthetic
      access: readwrite
    - name: orders-db
      sourceType: postgres
      access: read
  resourceCaps:
    cpu: "1"
    memory: 1Gi
  costCap:
    amount: "50.00"
    currency: EUR
    period: monthly
  purpose:
    category: internal-tool
    decisionsAboutPeople: false
  repoRef:
    url: http://gitea.tend-system.svc/tend-apps/expense-helper.git
    sandboxBranch: sandbox
    productionBranch: production
  builder: tend-builder
  tier: tier1

An observed status for an app of this shape looks like:

status:
  state: sandbox
  namespace: tend-app-expense-helper
  sandboxArtifact:
    image: harbor.tend-system.svc/apps/expense-helper@sha256:9f2c…
    attestationDigest: sha256:41ab…
    promptHash: sha256:77d0…
    model: anthropic.claude-3-5-sonnet-20241022
    builtAt: "2026-07-02T10:14:00Z"
  costStatus:
    accrued: "12.40"
    currency: EUR
    state: normal
    lastEvaluated: "2026-07-09T08:00:00Z"
    lastTransition: "2026-07-01T00:00:00Z"
  conditions:
    - type: Ready
      status: "True"
      reason: NamespaceProvisioned
    - type: Isolated
      status: "True"
      reason: SandboxIsolated
    - type: BuildSucceeded
      status: "True"
  observedGeneration: 3

Migration from 001

001 stored dataScopes as a []string enum whose only value was "synthetic". Governance depth (003) made DataScope a struct. A one-time, idempotent migration (hack/migrate-scopes.sh, unstructured read → rewrite; no conversion webhook) maps legacy "synthetic"{name: synthetic, sourceType: synthetic, access: readwrite} in both spec.dataScopes and status.promotion.declared.dataScopes, and must run before the struct-form CRD schema is applied.