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

> Turn a released Vexa version into a signed channel entry.

Source: `publisher/vexa_channel.py` · audience: publisher

## Overview

Emitted from `python3 publisher/vexa_channel.py --help`.

```text theme={null}
usage: vexa-channel [-h]
                    {fetch,build,verify,push,chart,refresh,revoke,platform-chart,platform-entry,sign-images,attest} ...

vexa-channel — turn a released Vexa version into a channel entry. The publisher consumes released
artifacts and receipts, never clusters, and holds no production credentials. Subcommands: fetch
gather the network-fetched inputs (release archive, provenance bundle, trusted root) via the gh
CLI into a directory build assemble and cross-check a channel entry from a release tag, the
candidate map at that tag, the internal delivery receipt, and the fetched inputs; write entry.json
+ evidence/ + VERIFY.md verify re-run every offline check against a built entry directory push
push a built entry to an OCI registry (or layout) via oras, sign it and its image digests with
cosign, and move the channel tag Every cross-check is named C1..C9; a failed check refuses the
entry (there is no silent path — an incomplete chain needs an explicit --break-glass record, which
becomes visible data in the signed entry). Two further checks guard the signing toolchain itself,
and they run inside the push path so they cannot be skipped: T1 the cosign that signs is inside
the pinned series. The signature LAYOUT is not stable across cosign majors, and the customer's
admission controller reads exactly one layout. T2 after signing, the signature is discoverable in
the shape Kyverno 1.19 will ask for — sha256-<digest>.sig in the signature repository — and
verifies against the channel key. A pin is a promise; T2 is the proof.

positional arguments:
  {fetch,build,verify,push,chart,refresh,revoke,platform-chart,platform-entry,sign-images,attest}
    fetch               fetch archive + provenance + trusted root via gh
    build               assemble and cross-check a channel entry
    verify              offline verification of a built entry
    push                push entry to OCI registry, sign, move channel tag
    chart               package the OSS chart with digest pins baked; optionally push OCI
    refresh             re-stamp an entry's expiry: same release, next seq, new horizon
    revoke              append to (or start, or re-publish) the channel's signed revocation list
    platform-chart      package the proprietary vexa-platform chart with cluster-captured digest
                        pins baked; optionally push OCI
    platform-entry      build a channel entry for a PLATFORM ESTATE (multi-chart, no OSS release
                        tag); requires a validation contract
    sign-images         cosign-sign every candidate-map digest into the channel signature repo
    attest              build + sign a structured attestation (in-toto statement) over the release
                        digests

options:
  -h, --help            show this help message and exit
```

## `fetch`

First step of a release crank: pull the release archive, its provenance bundle and the trusted root out of GitHub via `gh` into a working directory. Everything after this is offline, so this is the only step that needs the network to reach the OSS release.

```text theme={null}
usage: vexa-channel fetch [-h] --release RELEASE [--repo REPO] --out OUT

options:
  -h, --help         show this help message and exit
  --release RELEASE
  --repo REPO
  --out OUT
```

## `build`

Assemble the channel entry and run cross-checks C1..C9 against the release tag, the candidate image map at that tag, and the internal delivery receipt. A failed check refuses the entry — there is no silent path, only an explicit `--break-glass` record that becomes visible data inside the signed entry.

```text theme={null}
usage: vexa-channel build [-h] --release RELEASE --channel CHANNEL --entry-seq ENTRY_SEQ
                          [--supersedes SUPERSEDES] --vexa-repo VEXA_REPO
                          [--delivery-receipt DELIVERY_RECEIPT] --archive ARCHIVE
                          --provenance-bundle PROVENANCE_BUNDLE --trusted-root TRUSTED_ROOT
                          [--chart-ref CHART_REF] [--chart-digest CHART_DIGEST]
                          [--chart-version CHART_VERSION] --identity IDENTITY
                          --signing-mode {test_key,cosign_key,cosign_keyless}
                          [--signing-note SIGNING_NOTE]
                          [--publication-mode {dry_run,candidate,published}]
                          [--publisher PUBLISHER] [--approved-by APPROVED_BY]
                          [--approval-receipt APPROVAL_RECEIPT] [--break-glass BREAK_GLASS]
                          [--extra-evidence EXTRA_EVIDENCE] [--skip-cosign-verify]
                          [--expires-days EXPIRES_DAYS] --out OUT

options:
  -h, --help            show this help message and exit
  --release RELEASE
  --channel CHANNEL
  --entry-seq ENTRY_SEQ
  --supersedes SUPERSEDES
  --vexa-repo VEXA_REPO
  --delivery-receipt DELIVERY_RECEIPT
  --archive ARCHIVE
  --provenance-bundle PROVENANCE_BUNDLE
  --trusted-root TRUSTED_ROOT
  --chart-ref CHART_REF
  --chart-digest CHART_DIGEST
  --chart-version CHART_VERSION
  --identity IDENTITY   signing identity the entry declares
  --signing-mode {test_key,cosign_key,cosign_keyless}
  --signing-note SIGNING_NOTE
  --publication-mode {dry_run,candidate,published}
  --publisher PUBLISHER
  --approved-by APPROVED_BY
                        the named human approving publication (required for --publication-mode
                        published)
  --approval-receipt APPROVAL_RECEIPT
                        where that approval is recorded
  --break-glass BREAK_GLASS
                        actor=..,reason=..,approved_by=..,receipt=..
  --extra-evidence EXTRA_EVIDENCE
                        kind=filename=path, repeatable — e.g. soak=prod-
                        soak.v0.12.24.intoto.json=/path
  --skip-cosign-verify
  --expires-days EXPIRES_DAYS
                        freshness horizon in days (default 30). The entry declares it, every
                        verifier refuses it afterwards, and `refresh` extends it without a version
                        bump.
  --out OUT
```

## `verify`

Re-run every offline check against an entry directory without touching a registry. Use it after `build` to see what the entry claims before you push it, and to re-check an entry someone else built.

```text theme={null}
usage: vexa-channel verify [-h] --entry ENTRY [--archive ARCHIVE] [--pubkey PUBKEY]

options:
  -h, --help         show this help message and exit
  --entry ENTRY
  --archive ARCHIVE
  --pubkey PUBKEY
```

## `push`

Publish the built entry: push it to the registry with `oras`, sign it and every image digest with cosign, and move the channel tag. The signing toolchain checks T1/T2 run inside this path so they cannot be skipped — T2 proves the signature is discoverable in the exact shape the customer's Kyverno will ask for.

```text theme={null}
usage: vexa-channel push [-h] --entry ENTRY --ref REF [--channel-tag CHANNEL_TAG]
                         [--sign-key SIGN_KEY] [--skip-sign-artifact] [--plain-http] [--insecure]
                         [--signing-receipt SIGNING_RECEIPT] [--ledger LEDGER]

options:
  -h, --help            show this help message and exit
  --entry ENTRY
  --ref REF             repository ref without tag, e.g. host/base/channel/acme-stable
  --channel-tag CHANNEL_TAG
                        floating tag to move, e.g. `current`. A full ref
                        (host/base/channel/x:current) is accepted and normalised to its tag.
  --sign-key SIGN_KEY
  --skip-sign-artifact
  --plain-http
  --insecure            registry TLS is self-signed (test rigs)
  --signing-receipt SIGNING_RECEIPT
                        write the signing-run record (cosign version, flags, layout) as JSON
  --ledger LEDGER       checkout of the vexa-stations ledger; on a successful push the entry is
                        reduced into channels/<channel>/channel.yaml, which is the AUTHORITY for
                        entry_seq (the copy in the bucket is derived). Defaults to
                        $VEXA_STATIONS_DIR.
```

## `chart`

Package the OSS chart with the release's image digests baked in, so the customer's Argo resolves a tag to nothing and a digest to exactly one image. Push it to the channel's chart repository; the chart's semver IS the channel position a station follows.

```text theme={null}
usage: vexa-channel chart [-h] --release RELEASE --vexa-repo VEXA_REPO [--baseline BASELINE]
                          [--chart-version CHART_VERSION] --out-dir OUT_DIR [--push PUSH]
                          [--insecure]

options:
  -h, --help            show this help message and exit
  --release RELEASE
  --vexa-repo VEXA_REPO
  --baseline BASELINE   node-baseline values merged after the pins
  --chart-version CHART_VERSION
                        chart revision (Chart.yaml version); appVersion stays the release.
                        Default: the release version, the pre-2026-08-24 behaviour
  --out-dir OUT_DIR
  --push PUSH           oci://host/path/charts destination
  --insecure
```

## `refresh`

Re-stamp an entry's expiry — same release, next seq, new horizon — when the channel is still current but its entry is about to go stale. Every entry expires, so a channel nobody refreshes stops verifying on its own; that is deliberate, and this is the verb that says "still good" without republishing the release.

```text theme={null}
usage: vexa-channel refresh [-h] --entry ENTRY --out OUT [--expires-days EXPIRES_DAYS]
                            [--entry-seq ENTRY_SEQ] [--publisher PUBLISHER]

options:
  -h, --help            show this help message and exit
  --entry ENTRY         a built entry directory
  --out OUT
  --expires-days EXPIRES_DAYS
  --entry-seq ENTRY_SEQ
                        default: the refreshed entry's seq + 1
  --publisher PUBLISHER
```

## `revoke`

Withdraw a release or a digest from the channel by appending to its signed revocation list — or publish an EMPTY list, which is how the list goes live before it is needed. We can publish and cannot un-publish: this is the only path that stops a subscriber's sync, and the `--reason` is what their operator reads when it stops.

```text theme={null}
usage: vexa-channel revoke [-h] --ref REF --channel CHANNEL [--version VERSION] [--digest DIGEST]
                           [--reason REASON] [--severity {low,medium,high,critical}]
                           [--supersedes SUPERSEDES] [--advisory ADVISORY]
                           [--expires-days EXPIRES_DAYS] [--key KEY] [--unsigned] [--dry-run]
                           [--out OUT] [--plain-http] [--insecure]

options:
  -h, --help            show this help message and exit
  --ref REF             channel base ref, e.g. host/vexa/channel/pilot-stable
  --channel CHANNEL     channel name the list governs
  --version VERSION     release to withdraw, e.g. v0.12.23
  --digest DIGEST       exact artifact/image digest to withdraw
  --reason REASON       what an operator reads when their sync stops
  --severity {low,medium,high,critical}
  --supersedes SUPERSEDES
                        the release to move to instead, if one exists
  --advisory ADVISORY   link to the fuller writeup
  --expires-days EXPIRES_DAYS
  --key KEY             cosign key; the list is signed with the channel key
  --unsigned            local fixtures only
  --dry-run             write the list, publish nothing
  --out OUT             where --dry-run writes
  --plain-http
  --insecure
```

## `platform-chart`

The same packaging act as `chart`, for the chart nobody outside can see: the proprietary `vexa-platform` chart, pinned from digests captured off a running cluster rather than off a release tag. Reach for it when what ships is an estate we operate, not an OSS release — there is no upstream tag to read pins from, so the live namespace is the source of truth and anything that cannot be pinned through values has to be declared as unpinnable, with its reason.

```text theme={null}
usage: vexa-channel platform-chart [-h] --release RELEASE --chart-dir CHART_DIR
                                   [--pin-set PIN_SET] [--values VALUES]
                                   [--pins-values PINS_VALUES] [--unpinnable REPO=REASON]
                                   [--chart-version CHART_VERSION] --out-dir OUT_DIR [--push PUSH]
                                   [--insecure] [--no-verify-gate]

options:
  -h, --help            show this help message and exit
  --release RELEASE     the platform release this chart deploys (Chart.yaml appVersion)
  --chart-dir CHART_DIR
                        path to chart/vexa-platform (read-only; copied before edit)
  --pin-set PIN_SET     newline-delimited repo@sha256:... refs (or <values.path>=repo@sha256:...)
  --values VALUES       deploy-time overlay this chart is rendered with (repeatable, in order)
  --pins-values PINS_VALUES
                        OVERLAY MODE: bake these values files in as the pins and rely on P3,
                        instead of mapping a --pin-set through PLATFORM_IMAGE_PATHS (repeatable,
                        in order)
  --unpinnable REPO=REASON
                        declare an image that cannot be pinned through values, with the reason;
                        recorded in the report
  --chart-version CHART_VERSION
                        chart revision (Chart.yaml version); appVersion stays the release
  --out-dir OUT_DIR
  --push PUSH           oci://host/path/charts destination
  --insecure
  --no-verify-gate      do NOT inject the PreSync verify template. An estate packaged this way
                        reaches Argo CD with no signature, contract, revocation or approval check
                        at sync time — it exists for reproducing a chart published before
                        2026-08-25, and for nothing else.
```

## `platform-entry`

Build a channel entry for a whole platform estate — several charts, no single OSS release tag behind them. It refuses without a validation contract, because an estate with no stated scope is an estate nobody can prove anything about. What the estate is known NOT to cover goes in `known_holes` and rides inside the signed entry, where a subscriber reads it before they find out.

```text theme={null}
usage: vexa-channel platform-entry [-h] --spec SPEC --validation-contract VALIDATION_CONTRACT
                                   --release RELEASE --channel CHANNEL --entry-seq ENTRY_SEQ
                                   [--supersedes SUPERSEDES] --identity IDENTITY
                                   --signing-mode {test_key,cosign_key,cosign_keyless}
                                   [--signing-note SIGNING_NOTE]
                                   [--publication-mode {dry_run,candidate,published}]
                                   [--publisher PUBLISHER] [--approved-by APPROVED_BY]
                                   [--approval-receipt APPROVAL_RECEIPT]
                                   [--extra-evidence EXTRA_EVIDENCE] [--expires-days EXPIRES_DAYS]
                                   --out OUT

options:
  -h, --help            show this help message and exit
  --spec SPEC           YAML describing the estate: source commit, charts, images, absences
  --validation-contract VALIDATION_CONTRACT
                        the validation contract this estate was proven against; hashed into the
                        entry
  --release RELEASE
  --channel CHANNEL
  --entry-seq ENTRY_SEQ
  --supersedes SUPERSEDES
  --identity IDENTITY
  --signing-mode {test_key,cosign_key,cosign_keyless}
  --signing-note SIGNING_NOTE
  --publication-mode {dry_run,candidate,published}
  --publisher PUBLISHER
  --approved-by APPROVED_BY
  --approval-receipt APPROVAL_RECEIPT
  --extra-evidence EXTRA_EVIDENCE
  --expires-days EXPIRES_DAYS
  --out OUT
```

## `sign-images`

Sign every digest in the candidate map into the channel's signature repository. Separated from `push` because the signature layout — not the signature — is what the customer's admission controller reads, and a re-signing pass must be runnable without republishing the entry.

```text theme={null}
usage: vexa-channel sign-images [-h] --candidate-map CANDIDATE_MAP --key KEY
                                [--signature-repository SIGNATURE_REPOSITORY] [--receipt RECEIPT]
                                [--insecure]

options:
  -h, --help            show this help message and exit
  --candidate-map CANDIDATE_MAP
  --key KEY
  --signature-repository SIGNATURE_REPOSITORY
                        flat COSIGN_REPOSITORY for all signatures
  --receipt RECEIPT     write the signing-run record (cosign version, flags, per-image signature
                        tags) as JSON
  --insecure
```

## `attest`

Record a signed, structured claim about a release: a station's verdict, a soak result, anything a downstream contract can `require:` by name. This is how the internal channel accumulates evidence that the published channel entry is later built from.

```text theme={null}
usage: vexa-channel attest [-h] --kind {prod-soak,security-hardening,station-verdict}
                           --release RELEASE --vexa-repo VEXA_REPO --metrics METRICS --key KEY
                           --out OUT [--push PUSH] [--insecure]

options:
  -h, --help            show this help message and exit
  --kind {prod-soak,security-hardening,station-verdict}
  --release RELEASE
  --vexa-repo VEXA_REPO
  --metrics METRICS     JSON file with the predicate's measured fields
  --key KEY
  --out OUT
  --push PUSH           channel base ref; attestation lands at <ref>/attestations:<kind>.<release>
  --insecure
```
