> ## Documentation Index
> Fetch the complete documentation index at: https://delivery.vexa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Upgrade

> You already run Vexa: tell us your setup, get a bundle that fits it.

If you're on 0.10, that means you're ready for 0.12. ([Install](install) is the other door — a cluster with nothing in it yet.)

## You

1. Run the report generator.
2. Read the YAML file it writes.
3. Send it to us by email.

```bash theme={null}
git clone https://github.com/Vexa-ai/vexa-delivery
cd vexa-delivery
python3 kit/report/vexa_state_report.py --namespace vexa --dry-run   # the commands it will run
python3 kit/report/vexa_state_report.py --namespace vexa             # writes state-report.yaml
```

It writes one file. Here is a real one, from a live cluster, whole and unedited — this is what you would be sending:

<Accordion title="state-report.yaml — complete, 270 lines">
  ```yaml theme={null}
  # vexa-state-report 0.6.0 — the shape of this deployment's environment.
  #
  # WHAT THIS IS. The output of one read-only `kubectl get` sweep of a single
  # namespace. No database connection, no pods/exec, no credentials of any kind,
  # no SQL. Run `--dry-run` to see every command that produced it.
  #
  # WHY IT EXISTS. You already run Vexa. This says what shape your environment
  # is, so what gets built for you works with what you already have and asks for
  # nothing you do not.
  #
  # WHAT IS NOT IN IT. No schema, no rows, no counts, no meeting content, no
  # transcripts. No credentials, and no Secret or ConfigMap values — where a
  # setting comes from one, only its name is recorded. No node names, no service
  # addresses, no ingress hostnames.
  #
  # IT HAS NOT BEEN SENT ANYWHERE. The tool that wrote it has no transmit path:
  # no --submit, no destination, no endpoint. Read it, then send it by hand — or
  # do not.

  tool: vexa-state-report
  tool_version: '0.6.0'
  schema_version: 6
  generated_at: '2026-08-27T22:47:21Z'
  namespace: vexa
  read_with: kubectl get -o json, read-only. Nothing else was contacted.
  kit: af1725a-dirty


  # 1 · PLATFORM — what this runs on. Distribution and version, the cloud
  # underneath (read from the node providerID scheme; the instance ids are
  # dropped), the node shapes available to schedule on, and the storage classes
  # and volumes this cluster already has. Node NAMES are not collected: shapes
  # are grouped, which is the same fact without the inventory.
  platform:
    kubernetes: v1.35.7
    distribution: Kubernetes
    distribution_note: clusterversion unreadable — plain Kubernetes and a missing ClusterRole look the same
    cloud: Linode
    node_shapes_note: a taint needs a matching toleration in the values we hand back, or the pod stays Pending
    node_shapes:
      - count: 1
        instance_type: g6-standard-2
        region: us-sea
        capacity: cpu 2 (1930m allocatable) · memory 3.8Gi (2.7Gi allocatable) · pods 110
        taints: none
        gpu: null
        arch: amd64
        os: Debian GNU/Linux 13 (trixie)
        kubelet: v1.35.6
        runtime: 'containerd://2.2.6'
    storage_classes:
      - linode-block-storage · linodebs.csi.linode.com · Delete
      - linode-block-storage-retain · linodebs.csi.linode.com · Retain · DEFAULT
    volumes:
      - data-vexa-vexa-minio-0 · 10Gi · linode-block-storage-retain · Bound
      - data-vexa-vexa-postgres-0 · 10Gi · linode-block-storage-retain · Bound
      - vexa-vexa-agent-workspaces · 10Gi · linode-block-storage-retain · Bound
      - vexa-vexa-redis-data · 10Gi · linode-block-storage-retain · Bound


  # 2 · VERSIONS AND VALUES — every workload, the image and the digest ACTUALLY
  # running, replicas, the requests and limits each container declares, and the
  # settings it carries. The digest is what says 0.10 exactly rather than
  # roughly. Settings are allowlist-first: a name matching
  # password|token|secret|key|apikey is dropped, and so is anything not on the
  # allowlist, so what is here is the customisation and nothing else.
  # `provided_externally` names the settings that come from a Secret or
  # ConfigMap — the name only, never the value. `placement` is where a workload
  # is PINNED: the taints above say which nodes repel it, this says where it is
  # held. Shape only, and no line means it pins nothing.
  workloads:
    - kind: Deployment
      name: vexa-vexa-redis
      chart: vexa-0.12.35
      replicas: '1 ready of 1 desired'
      placement: affinity podAntiAffinity
      containers:
        - name: redis
          image: >-
            valkey/valkey:8-alpine@sha256:e0eb7c480958d32bdc4357a74bdd70653ae15f2f9b4c93c4a5a9fad1dc471c84
          registry: docker.io
          tag: '8-alpine'
          running_digest: 'sha256:e0eb7c480958d32bdc4357a74bdd70653ae15f2f9b4c93c4a5a9fad1dc471c84'
          requests: cpu 100m · memory 256Mi
          limits: cpu 1 · memory 1Gi
    - kind: StatefulSet
      name: vexa-vexa-minio
      chart: vexa-0.12.35
      replicas: '1 ready of 1 desired'
      placement: affinity podAntiAffinity
      containers:
        - name: minio
          image: >-
            minio/minio:latest@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
          registry: docker.io
          tag: latest
          running_digest: 'sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e'
          requests: cpu 100m · memory 256Mi
          limits: cpu 500m · memory 1Gi
    - kind: StatefulSet
      name: vexa-vexa-postgres
      chart: vexa-0.12.35
      replicas: '1 ready of 1 desired'
      placement: affinity podAntiAffinity
      containers:
        - name: postgres
          image: >-
            postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73
          registry: docker.io
          tag: '17-alpine'
          running_digest: 'sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73'
          requests: cpu 200m · memory 512Mi
          limits: cpu 4 · memory 4Gi


  # 3 · RESOURCES — the namespace's quotas and LimitRanges, and the containers
  # that declare no resources of their own. This is here because it broke a real
  # upgrade: a ResourceQuota covering cpu or memory makes Kubernetes refuse any
  # pod that declares neither request nor limit, unless a LimitRange supplies a
  # default. Vexa's per-meeting bot pods declare none by design. `finding` below
  # is present only when all three conditions hold at once.
  resources:
    quotas: []
    limit_ranges: []
    containers_declaring_no_resources: []


  # 4 · WIRING — which components exist and how they are connected. The database
  # and transcription especially: in-cluster or external, how each is ADDRESSED
  # (the setting's name, never its value), version, and GPU or CPU. Every fact
  # here was read from the cluster. Nothing connected to the database. Exposure
  # is read as Ingress AND as OpenShift `Route`, because an OpenShift estate has
  # no Ingress and reading one alone would say positively that nothing is
  # exposed.
  wiring:
    database:
      where: in-cluster
      workload: vexa-vexa-postgres
      version: '17-alpine'
      image: >-
        postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73
      resources: cpu 200m · memory 512Mi
      addressed_by: null
    redis:
      where: in-cluster
      workload: vexa-vexa-redis
      version: '8-alpine'
      image: >-
        valkey/valkey:8-alpine@sha256:e0eb7c480958d32bdc4357a74bdd70653ae15f2f9b4c93c4a5a9fad1dc471c84
      resources: cpu 100m · memory 256Mi
      addressed_by: null
    transcription: []
    exposure_note: 'type, class, ports and TLS below; no address, host or DNS name'
    services:
      - vexa-vexa-admin-api · ClusterIP · 8001/TCP
      - vexa-vexa-agent-api · ClusterIP · 8100/TCP
      - vexa-vexa-gateway · ClusterIP · 8000/TCP
      - vexa-vexa-meeting-api · ClusterIP · 8080/TCP
      - vexa-vexa-minio · ClusterIP · 9000/TCP,9001/TCP
      - vexa-vexa-postgres · ClusterIP · 5432/TCP
      - vexa-vexa-redis · ClusterIP · 6379/TCP
      - vexa-vexa-runtime · ClusterIP · 8090/TCP
      - vexa-vexa-terminal · ClusterIP · 3000/TCP
    ingresses: []
    routes: 'absent — an OpenShift kind; not-present and not-granted are the same read here'


  # 5 · REGISTRY — where images actually come from, so an upgrade does not sit
  # in ImagePullBackOff behind a corporate mirror. Observed from image
  # references, pull credentials on the pod specs AND on the ServiceAccounts,
  # and any readable mirror configuration — never inferred. Whether a pull
  # SUCCEEDS is unreadable and is asked as a QUESTION at the end.
  registry:
    registries_referenced:
      - docker.io (3 containers)
    image_pull_credentials:
      - vexa-channel-registry (on serviceaccount default)
    image_pull_credentials_note: 'the NAMES of the imagePullSecrets referenced; nothing in them is read'
    cluster_mirror_config: null
    cluster_mirror_note: >-
      ImageDigestMirrorSet is OpenShift and cluster-scoped, and was not readable
      (kubectl get imagedigestmirrorsets.config.openshift.io exited 1) — what a
      non-OpenShift cluster and a missing ClusterRole both look like. A
      runtime-level mirror is invisible to every kubectl read anyway.
    reachability: >-
      every image reference points at a public registry (docker.io), with 1 pull
      secret(s) attached. Whether the pull goes direct or through a transparent
      proxy is not observable from here.


  # 6 · ADMISSION — what this namespace will LET run: its Pod Security labels
  # and, on OpenShift, its SCC ranges. This decides whether what we deliver runs
  # non-root with a seccomp profile and all capabilities dropped — and, under
  # SCC restricted-v2, that it pins no UID of its own, since one outside the
  # range below is rejected rather than mutated. Unreadable is absent, never
  # permissive.
  admission:
    pod_security: >-
      no pod-security.kubernetes.io labels on this namespace — whatever the
      cluster's own default is applies, and this read cannot see what that is
    openshift_scc: 'no openshift.io/sa.scc.* annotations — what plain Kubernetes looks like'


  # 7 · NETWORK POLICY — whether anything here can reach a registry. A policy
  # that default-denies egress is why an install sits unable to resolve DNS or
  # pull an image, and it appears nowhere else in this document. Shape only:
  # rule bodies carry internal addresses and building a bundle does not need
  # them.
  network_policy:
    policies: []
    egress: no NetworkPolicies in this namespace — nothing here isolates it


  # 8 · THE INSTALL ALREADY HERE — the release name and what manages it, read
  # from the labels the workloads carry: no helm binary was run, no Secret was
  # read. An upgrade under a release name that matches nothing does not fail —
  # it installs a SECOND copy of the estate beside the running one, against the
  # same database, and reports success. Argo CD or Kyverno already here makes
  # this an adoption; `openshift-gitops` is checked too, because on OpenShift
  # the supported Argo is the operator's and a second beside it is that same
  # footgun. `cluster_scope` is derived from what this run was ALLOWED to read.
  install:
    release_names:
      - vexa (3 workloads)
    managed_by: Helm
    cluster_scope: nodes and storage classes readable — admission can ship cluster-wide
    argo_cd: present in namespace argocd · 6 deployments · version v3.5.1
    openshift_gitops: absent — namespace openshift-gitops exists but runs no deployments
    kyverno: present in namespace kyverno · 4 deployments · version v1.19.0


  # WHAT COULD NOT BE READ, AND WHAT CANNOT BE. Recorded rather than defaulted
  # to zero or an empty list: a fabricated zero in a document whose whole
  # purpose is to say what you already have is worse than a stated gap. Most
  # rows are reads this run was not granted; a row marked QUESTION is one no
  # read-only call can answer at all, so the answer is a sentence from you.
  absent:
    - what: transcription
      reason: >-
        no workload matched 'whisper|transcri|asr|diariz|stt' — widen it with
        --transcription-match. ABSENT, not 'CPU'.
    - what: routes
      reason: kubectl get routes.route.openshift.io exited 1 — how an OpenShift estate is exposed
    - what: 'QUESTION: can this cluster pull a release image by digest'
      reason: proving it needs a probe pod, which is a write
    - what: 'QUESTION: does the registry need a CA bundle, or plain HTTP'
      reason: TLS trust is in no object readable from here


  # WHAT THE TOOL REFUSES, each enforced by code rather than intent. Read the
  # source: it is one file, Apache-2.0, and `--dry-run` prints every command.
  refuses:
    - 'transmit: there is no submit path in this tool'
    - 'write: every call is kubectl get or kubectl version; no other verb exists here'
    - 'connect: no database client, no SQL, no flag that would take a password'
    - 'content: no schema, no rows, no counts, no meeting data, no Secret values'


  # THE REDACTION SELF-CHECK. `withheld_values` counts the values this run SAW
  # and chose not to record. The finished document is then scanned for every one
  # of them; `verified: true` means none survived. A survivor exits 3 and names
  # the count, never the value.
  redaction:
    rule: >-
      values under keys matching password|token|secret|key|apikey become
      REDACTED; settings capture is allowlist-first, so most values are never
      written rather than redacted after
    withheld_values: 3
    verified: true
    leaks: 0
  ```
</Accordion>

## Us

1. We build your bundle and give you [a channel](channel) — its name, its public key, and a pull-only credential.
2. You [install Vexa Delivery](install) in your cluster: pinned Argo CD, pinned Kyverno, the [admission policy](verify), after a [preflight](preflight) that fails closed.
3. You subscribe, and your staging syncs. [Promotion to production](operations) is yours to make.
4. It stays current from there, as we publish — [how that works](how-it-works), and [what's proven where](tested).

Next: [How it works](how-it-works) · [The channel](channel) · [Operations](operations)
