acme-stable in the examples below), your registry account name, your pull-only credential, age-encrypted, the channel public key channel.pub, and the signature repository the image signatures live in — see the channel. channel.pub is what every signature on this page is checked against; keep it, and pin it.
What you need
Preflight checks three of those five rows in step 2 — the version floor (P9), the default StorageClass (P8), and whether the delivered set fits your nodes, quotas and pod-security (P1, P2, P3, P4, P6) — and fails closed on every one of them, so you find out now rather than at first sync. Outbound reach it reads statically from your NetworkPolicies (P5), which cannot prove a route; proving one, and a real pull by digest (P7), needs
--live-probes and a namespace that already holds the pull secret, so that pass belongs after step 3. The LoadBalancer/ingress row is yours — no preflight check reads it, because how you expose the gateway is your cluster’s business.
What you open outbound
Four destinations, and nothing needs to reach in. Everything below is egress from the namespaces Vexa runs in; the subscription pulls, verification is offline, and no inbound rule exists for us.
Cluster-internal DNS must resolve, which is worth stating because a default-deny NetworkPolicy with no DNS allowance is the commonest way an install sits doing nothing — P5 checks for exactly that.
Step 1 — Get the kit
acme-stable with the channel name in your onboarding mail. A bad signature writes nothing to disk. The kit unpacks to ./vexa-kit, so read vexa-kit/... for kit/... in the steps below. Also: --version vX.Y.Z (default latest), --dest, --keep-tarball.
Step 2 — Preflight
--manifests is what makes P2 and P3 mean anything. Without it the preflight sees only the bot profile and not one workload the channel delivers — so a chart container asking more than your LimitRange ceiling passes here and is refused at admission after a green install, which is what happened on 2026-09-06 to a 4Gi postgres under a 2560Mi ceiling. my-values.yaml is the same file step 3 takes (start from kit/profiles/vexa/customer-values.example.yaml), and a credentialed channel needs helm registry login channel.vexa.ai first. Step 3 renders the chart itself, so this render is only for running the preflight on its own.
P2 compares every declared request and limit against your LimitRange’s max and min, in memory and cpu, names the object and the number admission will refuse it on, and prints the ceiling it compared against — so a pass records what was true when it was taken. P4 reads the effective admission: the PSA enforce level and the SCC annotations your namespace actually carries. A namespace that enforces neither is NOT EVALUATED, never a pass — nothing about hardened namespaces can be verified where nothing is enforced.
Eight of the nine checks run here — P1–P6, P8, P9 — and each names its remedy. P7 is the live pull-by-digest probe: it creates a pod and needs the namespace’s image-pull Secret, which step 3 writes, so run it after the install with --live-probes --pull-test-image <ref>. A FAIL stops the install (exit 1). A check your credential is not allowed to run — listing nodes or StorageClasses from a namespace-scoped account — reports NOT EVALUATED and exits 4: nothing failed, and something was never looked at. See Preflight.
Step 3 — Install
--claim-code <the six digits> --station <your-station> in place of --registry-user and VEXA_CHANNEL_PASS: the installer fetches the credential over TLS and writes it straight into the cluster, so it never reaches your screen, your shell history or a file. The code is single-use and lives about fifteen minutes. On a cluster that is already installed, kit/claim.sh does the same exchange on its own.
The call is scheduled with us, and the code is read to you on it. There is nothing to try cold: a code exists only for the fifteen minutes after somebody says it aloud, and until then --claim-code has nothing to present. If the endpoint answers that there is no claim service there, that is the URL and not your code — the code is untouched and still good.
Installs pinned Argo CD, pinned Kyverno, your admission policy and the subscription. my-values.yaml is yours (start from kit/profiles/vexa/customer-values.example.yaml), applied over every release and never leaving your cluster. Its shipped default keeps every delivered container under a 2560Mi per-container ceiling — the documented project shape — by capping the delivered database, which the chart otherwise asks 4Gi for; delete that block if your LimitRange clears the chart’s own largest ask.
Namespaces are create-only. If --staging-ns/--prod-ns already exist — the normal shape where a platform team pre-creates the project with its PSA labels, SCC annotations and rolebindings — the installer says so and writes nothing to them. It creates a namespace only when it is the thing creating it, so a security grant it does not own is never merged over or pruned.
If your cluster already runs Argo CD or Kyverno, say so with --argocd and --kyverno, which each take auto (the default), adopt, install or skip. auto looks before it writes: absent → it installs the pinned version; present at the pinned version → it adopts yours and applies nothing to it; present at another version → it refuses and names the version, before a single object is written. skip leaves that component entirely to you.
--argocd, --kyverno, --claim-code and --manifests are in kit v0.1.6 and later. bootstrap.sh fetches kit:latest, so take the git clone above until your channel’s latest is v0.1.6 or newer; on v0.1.5 those four flags exit 2.
Four of those flags are the difference between an install that syncs and one that does not, so none of them is optional against a credentialed channel:
Also:
--registry-ca <pem> for a private CA, --registry-insecure when Argo CD does not trust the registry’s certificate — a self-signed test rig, and also a corporate-CA proxy-cache, where the 2026-09-06 rehearsal recorded it as required: --registry-ca reaches Kyverno, but Argo CD has no CA-bundle path for OCI repos, so without this flag repo-server fails with x509: certificate signed by unknown authority. Then --staging-ns/--prod-ns, --dry-run. No CLI access (OpenShift, shared clusters)? Merge the signed station bundle chart into your config repo instead — see OpenShift. (Station bundle here means the machinery chart Vexa publishes into your cluster; the station-report.yaml of step 5 is the return leg, travelling the other way.)
Step 4 — Smoke, with a human in a real meeting
An unrecognized link fails immediately.
--non-interactive rehearses without the human phase: it records S3 as SKIP and the overall verdict stays PASS — it proves the tooling, not capture.
Add --flows only if your entry actually carries the flows tier. It is not in every chart, and against an entry that ships no flows-api the S4 check reports the tier as undelivered and fails the whole run for a component you were never sent. Left off, S4 records SKIP and the verdict reflects what you were sent.
Rung. This step is the intended procedure and the tool implements it, but no audio has reached the pipeline in any delivery run yet — bot dispatch, the pinned image and pod lifecycle are proven; capture is not. What’s proven, and where.
Step 5 — Send the station report back
pyyaml, and jsonschema as well if you use --submit. Step 4 needs pyyaml too, because --customer-values is how it reads your admin API token out of your values file — and that token is not optional: without the values file you must pass --admin-token instead, or smoke exits before it checks anything. Preflight (step 2) and the report writer are stdlib-only.
Runs steps 2 and 4; add --install for step 3 between them. Exit codes: 0 pass · 1 phase failed · 2 usage · 3 redaction leak.
Writes one file, station-report.yaml — your profile, your values with secrets redacted, the contract, and the receipts, each verbatim under a plain-English comment, with the sha256 of its own text at the foot. Read all of it, then send it — by hand, or with --submit, which pushes that same file to your own path on the channel using your own credential:
--submit-dry-run to actually send. Your contract.yaml must carry a report_scope — it is the clause that bounds what may leave, and a submit with no bound is refused locally. See the station gate.
Next: The station gate · Operations · Isolated networks