> ## 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.

# vexa_state_report.py

> What is running here, before you upgrade it.

Source: `kit/report/vexa_state_report.py` · audience: customer

## When you use this

Run it FIRST on a deployment that already exists. It is a read-only `kubectl get` sweep of one namespace — no database connection, no `pods/exec`, no credentials of any kind, no SQL — and it writes one commented YAML file describing the shape of that environment: platform and version, how the database and transcription are wired, per-container resources against the namespace's quotas and LimitRanges, the image digests actually running, the settings you have customised, where your images come from, the taints on your nodes and where each workload is pinned, how the estate is exposed (Ingress and, on OpenShift, Route), what your namespace's Pod Security labels and NetworkPolicies will let run, and the name of the release already installed. The few questions no read-only call can answer — whether a pull by digest succeeds, whether your egress policy lets DNS out — are asked by name rather than assumed. Start with `--dry-run`, which connects to nothing and prints every command it would run. It prints a path and stops: nothing transmits.

## Usage

Emitted from `python3 kit/report/vexa_state_report.py --help`.

```text theme={null}
usage: vexa-state-report [-h] --namespace NAMESPACE [--dry-run] [--kubeconfig KUBECONFIG]
                         [--context CONTEXT] [--out OUT]
                         [--transcription-match TRANSCRIPTION_MATCH] [--verify-redaction]
                         [--no-verify-redaction]

vexa-state-report — the shape of your setup, so what we ship fits it.

A READ-ONLY `kubectl get` SWEEP OF ONE NAMESPACE, WRITTEN TO ONE FILE. No
database connection, no `pods/exec`, no credentials of any kind, no SQL.

You already run Vexa. This reads the shape of the environment it runs in, so
the bundle we build for you works with what you already have and asks for
nothing you do not. If you are on 0.10, that is what tells us you are ready
for 0.12.

ONE FILE, AND THAT IS THE DESIGN. `state-report.yaml`. Not a directory, not an
archive, nothing to extract. The person who has to approve this before it
leaves their perimeter must read ALL of it, and a pile of JSON files is a
cross-referencing exercise rather than a read. YAML because the reader is a
Kubernetes engineer who reads it all day, and because it carries comments — so
the explanation of each section sits above the section instead of in a second
document that can drift from it.

WHAT IT COLLECTS, and the list is complete:

  1. PLATFORM     Kubernetes or OpenShift, its version, the cloud or distro
                  underneath, the node shapes available to schedule on, and
                  the storage this cluster offers
  2. WIRING       which components exist and how they are connected — the
                  database and transcription especially: in-cluster or
                  external, how each is addressed, versions, GPU or CPU — and
                  how the estate is EXPOSED: Ingress and, on OpenShift, Route,
                  because an OpenShift estate normally has no Ingress at all
                  and reading only one of them says "nothing is exposed"
  3. RESOURCES    requests and limits per container, plus the namespace's
                  ResourceQuotas and LimitRanges. This is not decoration: a
                  quota-controlled namespace stopped a subscriber's bot pods
                  being admitted, because they declare no resources of their
                  own and no LimitRange supplied a default
  4. VERSIONS     the image tags and digests ACTUALLY running, so the jump is
                  known exactly rather than assumed, and the PLACEMENT each
                  workload declares — node selector, affinity kinds,
                  tolerations, priority and runtime class, topology spread.
                  Taints say which nodes repel; these say where the workloads
                  are pinned, and transcription pinned to a GPU pool is the
                  case that breaks a bundle silently
  5. VALUES       the settings this deployment has customised, so what we ship
                  does not overwrite a deliberate choice
  6. REGISTRY     whether images come from Docker Hub or through a mirror —
                  reported as observed, never as inferred
  7. ADMISSION    what this namespace will let run: its Pod Security labels
                  and, on OpenShift, the SCC UID and group ranges. This is
                  what decides whether the delivered workloads need
                  runAsNonRoot, a seccomp profile and dropped capabilities
  8. NETWORK      the NetworkPolicies in the namespace, by SHAPE — whether
                  anything default-denies egress, which is what decides
                  whether this cluster can reach a registry at all. Rule
                  bodies are not read: they carry internal addresses and are
                  not needed to build
  9. INSTALL      the name of the Helm release already here, and whether Argo
                  CD — including the OpenShift GitOps operator's own instance
                  — or Kyverno already run. A `--release-name` that matches
                  nothing installs a SECOND copy of the estate beside the
                  running one, against the same database, and installing our
                  Argo beside an operator-managed one is the same footgun

NEVER COLLECTED: schema, rows, row counts, SQL of any kind, transcripts,
meeting content, credentials. The database appears here only as a COMPONENT —
engine, version, in-cluster or external, how it is addressed, its resources —
and every one of those facts is read from the cluster, never by connecting.

THREE REFUSALS, and they are the design rather than caveats on it.

1. IT DOES NOT TRANSMIT. There is no --submit, no destination flag and no
   endpoint constant anywhere in this file. It writes one file, prints its
   path, and stops. What leaves the perimeter leaves because a human read it
   and sent it.

2. IT DOES NOT WRITE, AND IT DOES NOT CONNECT. Every call is `kubectl get -o
   json` or `kubectl version -o json`. No other verb: no exec, no apply, no
   patch, no delete, no port-forward, no logs. There is no database client in
   this file and no flag that would take a password.

3. IT CARRIES NO CONTENT. There is no field anywhere below that could hold a
   customer's data, because nothing here reads any. Settings capture is
   ALLOWLIST-FIRST: a variable is excluded unless its name matches the
   allowlist, so redaction is the second net and not the only one. Secret and
   ConfigMap VALUES are never read; a `valueFrom` records only that the
   deployment expects one. Node names, service addresses and ingress
   hostnames are not collected either — they are inventory, not shape.

ABSENT OVER ZERO, everywhere. A source that could not be read is recorded as
absent with a reason. It is never defaulted to zero or to an empty list: zero
is a claim, and a fabricated zero in a document whose whole purpose is to say
what somebody already has is worse than a stated gap.

The `absent` list carries a second kind of entry, and deliberately: NAMED
QUESTIONS — the handful of things that decide what we build and that no
read-only call can answer at all. Whether a pull of a release image by digest
actually succeeds needs a probe pod, which is a write; whether a NetworkPolicy
lets DNS out needs the rule bodies, which this refuses to read; whether the
registry sits behind a corporate CA appears in no object here. Each is stated
as a question with the reason it cannot be read, because an unasked question
is indistinguishable from an answered one.

────────────────────────────────────────────────────────────────────────────
ADDING A COLLECTOR — this file expects to be edited by people we have never met

This tool runs in estates we have not seen, and the first thing it will do in
some of them is miss something. That is not a failure mode to apologise for,
it is the lifecycle: the engineer standing in front of the gap is the only
person who can close it, the kit is Apache-2.0, and a patch back is worth more
to us than a support ticket.

So a collector is deliberately small and self-contained:

    def collect_thing(ctx):
        '''One paragraph: what this is for and what it refuses.'''
        out = {}
        doc, err = ctx.kube.get("things")
        if doc is None:
            return ctx.absent(out, "things", err)      # absent, not zero
        out["things"] = [...]
        return out

...and then one line in SECTIONS at the bottom of this file, with the comment a
reader will see above it in the YAML. That is the whole contract:

  * take `ctx`, return a dict, and it becomes one block of state-report.yaml;
  * never raise to say "nothing here". Record absent with a reason. If you do
    raise, the driver names your collector, keeps every other section, and the
    report says which one failed — a broken collector must not cost an
    operator their whole run;
  * if you saw a value you decided not to write down, put it in
    `ctx.withheld`. The redaction self-check scans the finished document for
    everything in that set, so an allowlist you extended stays checkable;
  * BUDGET IS A FEATURE. This file is read end to end by somebody deciding
    whether to send it. Prefer one compact line to five nested ones; say each
    fact once. Roughly 200-300 lines, comments included, is the shape, and
    300 is a ceiling rather than a guideline — a section that would push past
    it summarises instead of growing, and says what it summarised;
  * ONE TEST FOR WHETHER IT BELONGS: is it one of the nine things above? Node
    shapes, quotas, ingress class, resource limits, GPU-vs-CPU, image digests,
    replica counts, allowlisted non-secret settings — yes. Anything describing
    their data — no, and no amount of usefulness changes that.
────────────────────────────────────────────────────────────────────────────

    python3 kit/report/vexa_state_report.py --namespace vexa [--dry-run]

Exit codes: 0 written · 2 usage · 3 redaction leak (the file is kept so it can
be inspected and reported, and it must not be sent). An unreadable resource is
NOT a usage error: it degrades to `absent` with a reason and still writes.

options:
  -h, --help            show this help message and exit
  --namespace NAMESPACE
                        the namespace the Vexa workloads run in
  --dry-run             print every kubectl command a real run would issue and what it would
                        write, then exit 0 having connected to nothing and written nothing. Paste
                        it into a change ticket.
  --kubeconfig KUBECONFIG
  --context CONTEXT
  --out OUT             a directory to write state-report.yaml into, or a filename to write
                        (default: the current directory)
  --transcription-match TRANSCRIPTION_MATCH
                        regex matching the transcription workloads whose runtime shape is captured
                        (default 'whisper|transcri|asr|diariz|stt')
  --verify-redaction    scan the finished report and refuse to finish if a withheld value survived
                        (default; exit 3)
  --no-verify-redaction

what this is, in one sentence

  A read-only `kubectl get` sweep of one namespace, written to one file. No
  database connection, no pods/exec, no credentials of any kind, no SQL.

why we ask for it

  You already run Vexa. This reads the shape of the environment it runs in, so
  what we build for you works with what you already have and asks for nothing
  you do not. If you are on 0.10, that is what tells us you are ready for 0.12.

what it collects

  1 platform    Kubernetes or OpenShift, version, cloud, node shapes with the
                taints on them and what is allocatable, storage
  2 wiring      which components exist and how they are connected — database
                and transcription especially: in-cluster or external, how each
                is addressed, versions, GPU or CPU — and how the estate is
                exposed: Ingress and, on OpenShift, Route
  3 resources   requests and limits per container, and the namespace's
                ResourceQuotas and LimitRanges
  4 versions    image tags and digests actually running, and where each
                workload is pinned
  5 values      the settings this deployment has customised
  6 registry    Docker Hub or a mirror — as observed, never inferred
  7 admission   the namespace's Pod Security labels and, on OpenShift, its SCC
                ranges — what it will let run at all
  8 network     the NetworkPolicies, by shape: whether anything default-denies
                egress. Rule bodies are not read
  9 install     the Helm release name already here, and whether Argo CD, the
                OpenShift GitOps operator or Kyverno already run

what you get

  state-report.yaml — one file, roughly 200-300 lines, every block carrying a
  plain-English comment above it. No directory, no archive, nothing to extract.
  You are meant to read all of it before any of it is sent.

what it enforces, and where to check it

  reads only    every call is `kubectl get -o json` or `kubectl version -o json`
                (Kube — there is no other verb anywhere in the file)
  no database   no client, no SQL, no flag that takes a password; engine and
                version are read from what is running (collect_wiring)
  allowlist     a setting is dropped unless its name matches the allowlist, and
                dropped anyway if it matches password|token|secret|key|apikey
                (env_allowed)
  no transmit   no --submit, no destination, no network client

  START WITH --dry-run. It connects to nothing and prints every command it would
  run, and what it would write.
```
