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

# Kit v0.1.7 — the installer stops writing to a namespace it did not create

> Five findings from a station install, fixed and published: create-only namespaces, a preflight that compares what admission compares, a path error that no longer wears the attack message, and a contract that hashes to one number.

**Date:** 2026-09-07, the day after the run that found them. **Channel:** the
pilot channel on `channel.vexa.ai` — the **live** registry. **Source:**
`Vexa-ai/vexa-delivery` `main` @ `4a82369`, clean detached checkout.
**Container work** ran on `bbb`, never on the laptop.

**Two sentences.** The [entry-5 station install](/receipts/2026-09-07-bbb-station-entry5-install)
recorded five findings and fixed none of them; they are fixed here, published as
kit `v0.1.7`, and the channel's `latest` tag now resolves to it. Three of the
five are one shape — **a write that reached a surface it does not own, and a
check that then reported the result as green** — which is the same invariant
this repository states about ledger surfaces, one level down.

<Note>
  The subscriber is not named. Their registry hostnames, project names and estate
  details are not in this document. The channel is written as `<pilot-channel>`.
</Note>

## The five, and what each one now does

### 1 · A namespace the installer did not create is not written to

`install.sh`'s `ensure_namespace` ran
`kubectl create namespace <ns> --dry-run=client -o yaml | kubectl apply -f -`
unconditionally. Applying a bare Namespace is a **three-way merge**, so against
a project the platform team pre-created it pruned

```
pod-security.kubernetes.io/{enforce,enforce-version,audit,warn}: restricted
openshift.io/sa.scc.{uid-range,supplemental-groups,mcs}
```

down to the single `kubernetes.io/metadata.name` label. Silently, on a namespace
the installer does not own — and a pre-created project carrying exactly those
labels and annotations is the **documented** shape for a shared cluster.

It is now **create-only**: the installer writes a Namespace object only when it
is the thing creating it, says so once per namespace when it is not, and touches
no label, annotation or rolebinding of a namespace that already exists.

**And the check that reported the result as green no longer can.** Preflight P4
returned PASS with *"no SCC and no PSA enforce label on the namespace —
admission here is permissive; nothing to trip, nothing verified about hardened
namespaces"*. A green obtained from the enforcement being absent, in the one
check whose subject **is** the enforcement. P4 now prints the effective
admission it read — PSA enforce level, whether the cluster serves SCC, which
`openshift.io/sa.scc.*` annotations the namespace carries — and a namespace that
enforces *neither* is **NOT EVALUATED**, never a pass.

### 2 · Preflight P2 compares what admission compares

The delivered postgres asks a 4Gi memory limit; the documented project shape
caps a container at 2560Mi, the figure sized for the meeting bot's 2Gi
memory-backed `/dev/shm`. Admission refused the database, the admin tier
crash-looped behind it, and the estate came up partly down — after a green
preflight.

**The receipt's diagnosis of *why* P2 passed was wrong, and the correction is
the more useful finding.** It read *"P2 checks that limits are declared, not
that they are under the LimitRange's `max`"*. It did compare, and a test pinned
that comparison. P2 passed because **the live `max.memory` was `8Gi` when the
preflight ran** — the session's own admin snapshot records `max.memory: 8Gi`
beside a `last-applied-configuration` reading `2560Mi`, a hand-widened value
that a later re-apply of the project's grant reset. The refusal arrived when
every pod was deleted and re-admitted under the restored ceiling.

So the check was **true when it was taken and false an hour later, and nothing
in its output could distinguish those two worlds**: it printed
`namespace has 1 LimitRange(s)` and no number. It now prints the numbers it
compared against, which is the same discipline this repository applies to
verdicts — positive evidence, not the absence of a complaint.

Three real holes were beside it, and all three are closed. The LimitRanger
checks `max` and `min` against the **request and the limit**, for every resource
the LimitRange names; P2 compared exactly one of those four pairs, in one
dimension. It now compares all four, in memory **and cpu**, and names the object
and the number:

```
StatefulSet/…-postgres 'postgres' declares limits.memory 4Gi above the
LimitRange max 2560Mi — admission will refuse the pod ('maximum memory usage
per Container is 2560Mi, but limit is 4Gi').
```

P3 sums cpu as well as memory, so a quota's `limits.cpu` is no longer a bound
nothing is compared against.

**And the delivered set now fits the documented project shape by default.**
postgres is the only container in the chart above 2560Mi; every other component
asks 1Gi or less. The shipped `customer-values.example.yaml` — the file the
installer renders with unless you hand it your own — caps it. The other road is
unchanged and still documented: if your platform team can size the project to
the chart's own largest ask, do that and delete the cap. Both are now checked by
P2 before the first sync rather than at admission.

### 3 · A path typo no longer wears the attack message

`vexa-verify.sh` chdirs into its workdir and only then resolves `--pubkey` and
`--policy`, so a **relative** path silently missed and the miss surfaced as

```
FAIL  entry signature does NOT verify against the pinned channel key
FAIL  revocation list signature does NOT verify against the pinned channel key
```

The identical entry verified `Verified OK` by hand one command later. The
script's own comment says a signature failure *"says someone may be attacking
you"* — which makes it the worst available message a mistyped path can produce,
because the subscriber's next move is to call their security team.

Both paths are resolved before the chdir and before anything is pulled, and a
file that is not there is named as what it is: `pubkey file not found: <path>`,
exit 2, with no signature language anywhere near it. A `--policy` that cannot be
read refuses too — it used to degrade to *no contract*, which turns every
contract check into a check that did not run.

### 4 · The bytes the gate hashes are the bytes the ledger holds

`install.sh` wrote the contract ConfigMap through
`json.dumps(yaml.safe_load(file))`, re-serializing a 757-byte record into 617
bytes of compact JSON. The in-cluster gate hashes the copy it is mounted, so the
recorded verdict named one hash while the ledger record — and the station
report's own `contract_document` section — hashed to another. An audit asking
*which promise was this admitted under* could not answer from either side.

A JSON contract is now mounted **verbatim**. YAML is still converted, because
the gate reads the document with `jq`, and then **both** shas are recorded in
the ConfigMap so the converted document traces back to the bytes a human signed
off. The installer prints the sha it wrote, which is the number the verdict will
carry.

This is the same defect the chart template fixed with a double-quoted scalar
after the 2026-08-29 station lost a verdict to a single `0a`. **It was the other
write path, and it still had it.**

### 5 · The ledger records the position the report carries

The ingest reducer read `targetRevision` out of a values file and then a
`position` key that appears in no schema. It never read the station report's own
`release` block — which carried the entry sequence, the entry digest, the chart
version and the chart digest — so a report saying exactly where a station stands
reduced to `subscribed_position: unknown` and `entry_seq: null`, and the
staleness flag could not fire for it.

It now reads that block, most specific source first, and **cross-checks** the
entry identity against the ingest receipt rather than silently ranking two
sources for one fact. A disagreement is recorded as a flag, because it is a
finding about the submission.

## What was published

|                        |                                                                                                                                                     |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ref**                | `channel.vexa.ai/vexa/channel/<pilot-channel>/kit:v0.1.7`                                                                                           |
| **Digest**             | `sha256:8a5e5e44a19b6a1e2676f60d43cd9a382c05313c301698af6494ed5829e3f855`                                                                           |
| **Layer / tgz sha256** | `dc0933884f0a27156fc0300d970734102ba0baaf43e3cf0433e3957e38d17f05` — 270381 bytes                                                                   |
| **Artifact type**      | `application/vnd.vexa.kit`                                                                                                                          |
| **Built**              | `2026-09-07T15:11:39Z`, the marker written into the package's `VERSION`                                                                             |
| **Pushed with**        | `oras 1.2.3`, publisher credential already resident on the host — never argv, never printed, never copied                                           |
| **Signing**            | cosign **2.6.5**, key mode, `--new-bundle-format=false --use-signing-config=false --tlog-upload=false`                                              |
| **Signing key**        | the channel's own key — the same one that signs every entry, every earlier kit, the chart and the verifier. **One key, this channel's whole life.** |
| **`latest`**           | moved to `sha256:8a5e5e44…`; it had pointed at v0.1.6                                                                                               |
| **Predecessors**       | `v0.1.0 … v0.1.6` left standing                                                                                                                     |
| **The channel itself** | untouched. No entry was published, no pin moved, `:current` still resolves to what it did before                                                    |

`kit/release.sh --dry-run` ran first. It packaged 270**382** bytes against the
real run's 270**381**; the difference is the `built=` timestamp inside `VERSION`,
written per run. The tree packaged was the same tree — worth saying rather than
claiming a byte-identical dry run that did not happen.

## Proof 1 — the version tag and `latest` name one digest

```
oras resolve …/kit:v0.1.7  -> sha256:8a5e5e44a19b6a1e2676f60d43cd9a382c05313c301698af6494ed5829e3f855
oras resolve …/kit:latest  -> sha256:8a5e5e44a19b6a1e2676f60d43cd9a382c05313c301698af6494ed5829e3f855
oras resolve …/kit:v0.1.6  -> sha256:f885bbae2efeae28ea778e8e47ae0ba0a2ddc7bc28404b08de9139909ed824fb
```

`latest` is moved with `oras tag` against the same descriptor bytes, so the
signature made over the digest carries without a second signing operation. The
predecessor still resolves to its own digest, unmoved.

## Proof 2 — it verifies against the channel's pinned key

```
cosign verify --key channel.pub --insecure-ignore-tlog=true \
  channel.vexa.ai/vexa/channel/<pilot-channel>/kit@sha256:8a5e5e44…
→ The following checks were performed on each of these signatures:
    - The cosign claims were validated
    - The signatures were verified against the specified public key
```

`cosign tree` on that digest shows **exactly one signature**, one layer — not a
second stacked beside it.

**The control:** kit `v0.1.6` still verifies against the same key. One key
verifies both, which is what makes *"one key for this channel's whole life"* a
check rather than a claim. `--insecure-ignore-tlog=true` is correct here rather
than a weakening: the channel signs offline against a pinned key and uploads
nothing to a transparency log, which is what makes it air-gappable, and it is
the same form `kit/bootstrap.sh` runs on the subscriber's side.

## Proof 3 — a subscriber-shaped pull gets a verified v0.1.7, with the fixes in it

Run from a **clean directory** with `HOME` and `TMPDIR` pointed at it, so the
publisher credential this same host had just pushed with was not reachable — the
isolated `HOME` has no `.docker` directory at all, checked after the run — and
with a **pull-only** credential supplied on stdin of a script already resident
on the host, so it never reached argv, a file or a transcript.

```
== clean dir: … (HOME=…, no publisher credential reachable)
== resolving channel.vexa.ai/vexa/channel/<pilot-channel>/kit:latest
   digest sha256:8a5e5e44…
== cosign verify …@sha256:8a5e5e44… against channel.pub
   signature OK
vexa-kit v0.1.7 unpacked to …/vexa-kit
  verified sha256:8a5e5e44… against channel.pub before unpacking
```

The ordering is the security property and it held: the signature is checked
before a single byte is unpacked. The isolation is not decoration — without it
the pull would have quietly authenticated as the publisher and proven nothing
about the subscriber's path.

| Check                           | Result                                                                                          |
| ------------------------------- | ----------------------------------------------------------------------------------------------- |
| unpacked `VERSION`              | `version=v0.1.7`, `built=2026-09-07T15:11:39Z`                                                  |
| unpacked `.kit-source`          | records the digest above and `version=latest` — what a subscriber who names no version receives |
| pinned key copied into the tree | reads back as the channel's own key                                                             |

**And the fixes are in the artifact, not only in the branch** — grepped in the
unpacked tree, which is bytes that came off the registry:

| Fix                                          | Found in the unpacked tree                           |
| -------------------------------------------- | ---------------------------------------------------- |
| 1 · create-only namespace                    | `install.sh` — `already exists — NOT touched`        |
| 2 · P2 prints its ceiling                    | `preflight/vexa_preflight.py` — `compared against —` |
| 2 · the database fits the documented project | `profiles/vexa/customer-values.example.yaml`         |
| 3 · pubkey path                              | `verify/vexa-verify.sh` — `pubkey file not found`    |
| 4 · contract sha printed                     | `install.sh` — `contract.json sha256`                |

`python3 preflight/vexa_preflight.py --help` runs from the unpacked tree.

## What this pass does NOT claim

* **Nothing was installed from this kit.** Pull, verify, unpack, shape and
  content are proven. `install.sh` was not run against a cluster here, so the
  five fixes rest on their own tests — a fake pre-created namespace, the
  contract bytes, path resolution, the LimitRange cases and the reducer cases —
  and on their presence in the published artifact. **The next station install is
  what closes that**, and it is the one that will exercise finding 1's fix
  against a real pre-created project.
* **No cluster was read or written in this pass**, and no meeting was captured,
  so nothing here says anything about the release's quality.
* **The pull-only credential was an existing account of ours**, not a freshly
  minted throwaway; the credential tool still cannot mint one against the live
  channel. Reads are not scoped per account at the edge — only writes are — so
  the path exercised is the one a subscriber takes, but this pass did not
  exercise credential issue and revoke.
* **No entry, pin or contract moved.** No credential was minted or rotated, and
  the signing key stayed in tmpfs on the build host for the length of the run.
