channel.vexa.ai/vexa/channel/pilot-stable — the live
registry, not a test rig. Cluster: LKE vexa-rehearsal (id 646952),
us-sea, k8s v1.36.3, 2× g6-standard-4, tag throwaway, created and
destroyed the same day. Container work ran on bbb, never on the
laptop.
The question this rehearsal was built to answer is not “does the machinery
work”. It is “if we send the kit to the pilot subscriber on Monday, what happens?” So it was
run the way they will run it: from an empty directory, with a pull-only
credential, against the real channel, with nothing borrowed from this
repository.
The short answer is that the customer would have got stuck four separate
times before a single Vexa pod started, and none of the four would have
looked like our fault to them. Every one is now fixed and proven. Three things
remain open and are named at the bottom.
What was proven, in order
1 · The entry on the customer channel was not one we could send
Before anything else, the entrypilot-stable:current was pulled and read. It
carried:
Decision taken, and why. The brief allowed for minting an entry at the
chart’s lineage. There is no honest way to do that:
v0.12.23 is the newest
tag that exists, and it is the only one with a real candidate map and a real
delivery receipt. So the entry was rebuilt at v0.12.23 from genuine inputs
— nothing fabricated, no --break-glass — and the chart was repackaged by
the publisher from that tag and republished, so the entry names a chart that
exists and was built from the release it claims.
vexa_channel.py verify then passed all ten offline checks including the
entry signature and the SLSA provenance against the archive bytes.
The 0.12.30-vs-0.12.23 “lineage mismatch” turned out to be a symptom of a
design defect, not a mistake by whoever published it. cmd_chart wrote the
release version into both of Chart.yaml’s version lines. Helm keeps two
because they are two facts — version is the chart revision a subscriber’s
targetRevision: "*" ranks, appVersion is the release it deploys. Collapsed,
a release could never ship a second chart revision, and chart revisions shared
a number space with releases. --chart-version separates them.
2 · VERIFY.md documented a command that has never worked
The publishedVERIFY.md told the customer to run:
--insecure-ignore-tlog=true, and --new-bundle-format must be
absent because the bundles are legacy. Both write_verify_md and the
publisher’s own cmd_verify carried the wrong flags, so the publisher’s
verify refused genuine entries.
Recorded working commands, verbatim, against the live channel:
cosign here is v3.1.3, not 2.4.3. It has --use-signing-config, and in
3.x it requires it: --tlog-upload=false is refused on its own with
"--tlog-upload=false is not supported with --signing-config". Signing needs
both --tlog-upload=false --use-signing-config=false. Both flags are
already deprecation-warned; a cosign bump will need this revisited.
VERIFY.md now also tells the reader to check the key they pin against the
identity the entry names — the defect in §1 existed precisely because nothing
did.
3 · The kit had never been published to a real registry
kit/release.sh aborted at the first push:
${PLAIN[@]} and ${COSIGN_INSECURE[@]} are empty unless --insecure or
--plain-http is passed, and an empty array under set -u is “unbound
variable” on bash 3.2. So the scripts worked only on the plain-HTTP test rig
and broke on exactly the path a real TLS registry takes. install.sh had
already learned this and carries the ${arr[@]+"${arr[@]}"} idiom in a
comment; release.sh, bootstrap.sh and self-update.sh had not.
Underneath it sat a second one: all four scripts and cosign_env() wrote
{"auths": {}} to defeat a hanging Docker credential helper. That is correct
against a public registry and fatal against an authenticated one — the
entry pushed and its signature failed UNAUTHORIZED. The hazard is the
helper, not the credentials. They now carry the on-disk auths across, drop
credsStore, and accept an explicit credential from
VEXA_CHANNEL_{REGISTRY,USER,PASS} — environment, never argv.
After the fix, published to the live channel: kit v0.1.0 … v0.1.5, each
signed, each moving latest.
4 · A customer with nothing but a credential gets a verified kit
A clean directory holding onlybootstrap.sh and channel.pub, run with
HOME and TMPDIR isolated and only the pull-only subscriber credential:
5 · Install: authenticated channel, and a Kyverno flag that lies quietly
install.sh had no way to give Argo CD or Kyverno a registry credential.
The Argo repository Secrets carried url/name/type and nothing else, so against
channel.vexa.ai the repo-server 401s and the subscription never syncs. Added
--registry-user (password from VEXA_CHANNEL_PASS).
Kyverno needed the same, and cost the most time for the smallest reason. The
controller flag takes a bare secret name resolved in Kyverno’s own
namespace. Passing kyverno/channel-registry-creds is accepted silently and
the fetch stays anonymous, which surfaces as:
6 · The channel flows, digest-pinned, into a fresh cluster
vexa-enterprise-staging resolved * to chart 0.12.31, pulled it from the
authenticated OCI registry, and reached Synced / Healthy with 13 pods:
admin-api ×2, agent-api, gateway, meeting-api ×2, runtime, terminal ×2, plus
postgres, redis, minio and its init Job. vexa-enterprise-prod tracked
UNPINNED and synced nothing — the customer-held gate, working.
7 · The station loop was broken at the join
kit/validate produced station.tar.gz, redaction verified — 4 values
removed, 0 found anywhere in the archive that was written. The publisher then
refused it:
validate names its receipt smoke-receipt-20260824-165142.md, because an
operator runs smoke more than once. And station.json carried neither the
station name the ingest checks --station against, nor the files[]
digest list its S3 check reads — it wrote a contents list of bare names
instead. Every genuine bundle was refused, and had always been. The ingest
had never seen one.
Fixed on both sides, keeping the load-bearing half intact: validate emits
station (new --station flag) and files[] with digests; ingest accepts
the dated receipt name while still refusing an ambiguous match. Then:
8 · The station gate refused the chart, and was right to
:latest — and the Kyverno
digest-pinning policy never caught them because it matches *vexaai/*.
Three were pinnable from kit/profiles/vexa/node-baseline.yaml and now are, at
digests resolved on the day. The fourth, minio/mc:latest, was hardcoded in
the chart template with no value to override, and the same Job declared no
resources at all — the exact LimitRange-squeeze class the preflight’s P2 exists
to catch. That needed a change in the OSS chart:
Vexa-ai/vexa#1321.
Proven that the fix closes the finding: a chart packaged from the v0.12.23 tree
with #1321 applied (in a throwaway clone, local tag, never pushed) gates
clean —
--extra-evidence other=<name>=<path> carries it into the signed entry. One fix was required —
every attached file was stamped application/json, so a markdown gate report
would have made the entry lie about its own evidence. Media type now follows
the extension, and entry seq 3 carries the real report:
9 · Pull-only self-upgrade: 26 seconds, zero kubectl
postgres:17-alpine@sha256:18cfe3ef… live in the StatefulSet. Nothing on the
vendor side touched the cluster; the cluster pulled.
10 · Kit self-update
Run five times over the day, each time as the customer, each time verifying the signature against.kit-source.pub before unpacking:
11 · The PreSync verify gate, switched on for the first time
It had been off in every install to date. Turning it on found four defects in a row, each of which fails the sync for a reason that has nothing to do with the evidence:
Defect 4 is the one worth losing sleep over. It is the gate’s loudest possible
alarm — this release may be forged — fired on a good release, by our own
wrong flag.
With verifier v0.1.1 (built on
bbb, linux/amd64, pushed and signed):
require_evidence_kinds += soak):
Failed. That a failed verifier stops the sync was observed
separately and involuntarily, during defect 4: the Application stayed
OutOfSync and did not advance until the verifier passed.
12 · mail_cursor — the question does not arise
A fresh install creates six tables and none of them is mail_cursor:
ALTER TABLE mail_cursor ADD COLUMN token TEXT (per Vexa-ai/vexa#1318) is
not needed on a fresh install of this chart, and cannot be applied: the
flows schema is not provisioned at all, because the flows tier is not part of
the delivered set. Confirmed independently by smoke S4 (below). The ALTER is a
question for an existing flows deployment, not for the pilot subscriber’s first install.
What FAILED or was partial
S3 — the bot joined nothing, and the cause is our own tenant
The M365 rig produced a real Teams meeting via Graph withlobbyBypassSettings: {scope: everyone} accepted by the API. The bot pod
spawned from the correct digest-pinned image
(vexaai/vexa-bot:v0.12.23@sha256:2bd879c6…, 1.6 GB, pulled in 33s) and failed:
completion_reason: join_failure, bot_outcome: never_admitted,
segments_captured: 0.
This is a rig-tenant policy, not a per-meeting setting and not a product
defect. lobbyBypassSettings governs the lobby; it does not grant anonymous
join, which is a Teams meeting policy on the organiser
(AllowAnonymousUsersToJoinMeeting). Changing it is a tenant setting change and
was not made. S3 is therefore PARTIAL: dispatch, image, pod and error
reporting all proven; capture not proven, and no audio question was ever
reached. Making S3 genuinely hands-free needs that policy flipped on the rig
by a human, once.
Worth noting for its own sake: the product recorded a full, correct
ExitReason and provenance block. This is precisely the per-reason detail that
an internal taxonomy note (private)
says the aggregate snapshot throws away.
S3, first attempt — a default install cannot run a meeting at all
Before the join was even attempted:customer-values.example.yaml mentions transcriptionServiceToken only as a
commented-out line reading “from your subscription pack”, and never mentions
transcriptionServiceUrl at all. Nothing in preflight, install, or the docs
tells an operator this is mandatory before anything can work. A customer
following the documented path installs successfully, reaches Synced/Healthy,
and then cannot run a single meeting. Supplied by hand here from the dev
backend to get past it. This is a customer-facing gap and it is still open —
see below.
S4 — the flows tier is not delivered, and used to crash the run
--flows ended the entire run in a Python traceback: no
receipt, no verdict, and an operator with no way to tell “not delivered” from
“broken”. The station bundle produced by that run was missing its smoke
receipt entirely.
The agent/model tier
Not exercised. The flows tier is not in the delivered set, so the question of whether a model credential exists for it never arose. Recorded as a known gap, not tested.Kyverno signature admission — BLOCKED, and this is the headline
The customer-side admission gate — the thing that is supposed to verify our attestations independently of us — cannot read the signatures we publish. The mechanism, isolated:
Confirmed both ways on the live channel.
--registry-referrers-mode=legacy
does not change it; --new-bundle-format=false does not change it. The
signature is present and correct — COSIGN_REPOSITORY=… cosign verify passes —
and Kyverno cannot see it.
The consequence for a customer is the worst available shape: a correctly
signed release is denied at admission with a message that says it is
unsigned. For the rehearsal to continue past this point, the
vexa-verify-channel-signature ClusterPolicy was deleted, and everything
after §6 was proven with image-signature admission OFF. That is recorded
here rather than smoothed, and nothing downstream should be read as
“admission-verified”.
The fix was not attempted because the obvious route — signing the images in
place on docker.io/vexaai/* — writes to a public namespace and is a
publishing act that needs its own decision.
Fixes this rehearsal forced
In this repository, on branchrehearsal-receipt:
In
Vexa-ai/vexa: #1321 — the
minio-init Job was unpinnable (minio/mc:latest hardcoded in the template) and
declared no resources.
make test and make lint green.
What the customer must do differently
- Set the transcription backend before installing.
transcriptionServiceUrlandtranscriptionServiceToken. Without both, the install succeeds and no meeting can ever run. This belongs ininstall.mdxand incustomer-values.example.yamlas a required field, not a comment. - Pass
--registry-userand exportVEXA_CHANNEL_PASS. Every enterprise channel is authenticated; without it Argo never syncs. - Expect
enterprise-prodto sit atUnknownwithimproper constraint: UNPINNEDuntil they move their own pin. That is the gate, not a fault, but the error text reads like one. - Do not enable image-signature admission yet. It will deny correctly signed images. Digest-pinning admission is fine and should stay on.
What this does NOT prove
- Nothing about transcription quality, or about capture at all. No audio ever reached the pipeline. S3 proved dispatch, image identity, pod lifecycle and error reporting — and stopped there.
- Nothing about image-signature admission. It was deleted to proceed. Every result from §6 onward was obtained with it off.
- Nothing about the flows/agent tier, which is not in the delivered set.
- Nothing about OpenShift. One provider profile (
lke) was exercised. - Nothing about the chart the customer would actually receive passing its own
gate. The gate passes only against a chart carrying the unreleased #1321.
On the real
v0.12.23tree it refuses, correctly, and the entry now on the channel carries that refusal as evidence. - Nothing about scale, upgrade-under-load, rollback, or break-glass. One install, one upgrade, one cluster, no failure injection.
- Nothing about the dependency images’ provenance. They are pinned now; pinned is reproducible, not attested.
- The 26-second upgrade is one measurement on a two-node cluster with a warm image cache — a demonstration that the transport works unattended, not a latency guarantee.
- Nothing about the LKE volume lifecycle beyond one teardown.
publication.approved_bynames the founder for an approval given to run this rehearsal, recorded inapproval_receiptpointing at this file. It is not an approval of v0.12.23 for the pilot subscriber’s production.
13 · The state the channel was left in
Publishing five chart revisions over the day lefttargetRevision: "*"
resolving to 0.12.35 while the entry’s chart.digest named 0.12.32 — the
subscriber would have run a chart its own entry does not describe. A rehearsal
that leaves that behind has broken the thing it was checking, so the channel
was made coherent before the session ended:
The hand-packaged
0.12.30 — the chart matching no release that started
this whole investigation — was deleted, along with the four superseded
rehearsal revisions. Final check with the commands VERIFY.md now prints:
Cluster
Destroyed. LKE 646952 deleted at the end of the session. No failure required keeping it. And its volumes, which the cluster deletion does not take with it. LKE’s default StorageClass islinode-block-storage-**retain**; the four PVCs this
install created survived the cluster and had to be detached and deleted by
hand, and the API reported them still attached to an already-deleted node for
about four minutes — a detach/delete retry loop is required, not a single
call. The lke provider profile already warns about this class (measured: 69
stranded PVs, $888/yr) and it is right to. 32 stranded volumes remain on the
account from earlier sessions — not from this one, and not cleaned here
because deleting other people’s data is not this agent’s call.
A customer running the kit on LKE inherits the same behaviour and should be
told: deleting the cluster does not delete the data, or stop the bill.