Files
universe/deploy
zeekayandHanzo Dev a32988281e lux-bank runs bankd v1.0.18, and Lux ID is the one sign-in the suite proves
bankd v1.0.18 takes only a lux.id access token issued to lux-financial for
the lux org, maps only a member of the admin org to _superusers, and listens
on HTTP alone. Its pod reads none of the bank-secrets keys, so the five env
references to them are gone.

bank-docs states the amd64 node selector its pod already runs with.

e2e/40-lux-financial proves the one way in: neither console serves a password
input or ships a credential in its JS; every sign-in screen hands off to lux.id
with client lux-financial, PKCE S256 and the console's /auth/callback; bankd
answers 404 on every password and Base sign-in route, the code exchange takes
only lux-financial by code or refresh, and a token lux.id did not issue to
lux-financial reads as anonymous. The password-form sign-in and the seeded
ledger totals it asserted are deleted: neither exists.

k8s/lux-k8s/{bank-api,bank-dash,bank-docs,bank-sandbox,bankd,financial} and
scripts/seed-bank-admin.ts are deleted. deploy/lux-bank declares the bank,
lux.financial is a static site, and no host those files name resolves.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 05:39:03 -07:00
..

deploy — the fleet, and the only thing that deploys it

Every service is one values file:

deploy/<namespace>/<name>.yaml

That is the whole inventory. Adding a service is adding a file; there is no second list to keep in step with it, and pin.sh --which reads the answer to "who runs this image" out of the directory rather than out of a table.

Each file is values for one shared chart, referenced by version:

oci://oci.hanzo.ai/charts/app:0.1.11
helm template <name> oci://oci.hanzo.ai/charts/app --version 0.1.11 \
  -n <namespace> -f deploy/<namespace>/<name>.yaml

What actually runs

The lux-fleet generator and its fence live in hanzo/universe infra/k8s/hanzo-cd/project-lux.yaml; they read this repo from GitHub and deploy to the cluster named lux-cluster. a git generator over deploy/*/*.yaml, one Application per file, destination lux-cluster, and a templatePatch that reads each file's own cd.automated:

syncPolicy:
  automated:
    prune: false
    selfHeal: true

19 of the 122 values files set cd.automated: true: the static sites, each verified answering from the edge on the current cluster. Every pod-based file is false until it is shown to run there; a pin bump does not opt a file back in. This file previously said the ApplicationSet "deliberately has no automated block" and that "there is no CD in this repository yet"; neither has been true since the templatePatch was added, and reading it that way is how a hand-edited object gets healed back and nobody knows why.

prune: false throughout. Nothing here deletes: a values file that stops declaring an object leaves that object running, which is why removing a service is a deliberate act on the cluster and not a git rm.

Moving an image — pin.sh

deploy/pin.sh <namespace>/<service> <version>   # a release
deploy/pin.sh --verify [<service>…]             # every pin, against its registry
deploy/pin.sh --which <repository>              # who runs this image
deploy/pin.sh --freeze <service>…               # a moving tag names bytes

cd.hanzo.ai does not watch the registry — it watches this directory. Publishing an image changes nothing; moving the pin here is the deploy. .hanzo/workflows/ image-published.yml is the only automated caller, and it does nothing else.

A pin is two fields and one fact. tag is how a human says which release; digest is what the kubelet pulls. The chart renders repo:tag@digest, and a reference carrying both resolves by digest — so a tag written from one registry read and a digest from another can name two different releases, and the pod then reports the tag while running the digest. pin.sh resolves both from one HEAD /v2/<path>/manifests/<tag> and writes them in one edit.

That is not hypothetical here. zoo/exchange-api pinned 2.3.34 to a digest that 2.3.34 no longer resolves to, beside three other deployments of the same version with no digest at all: four pods, one version number, two sets of bytes.

These files were generated from what is RUNNING

Not from the manifests in k8s/. Those were applied by hand over a long period and their currency is unknown. The running fleet is the only statement of what is true, so it is what was read.

Where the two disagreed, k8s/ was the stale one — and where k8s/ held something these files did not (an IngressRoute, a ConfigMap, a claim, a Role), that object moved here and the manifest was deleted. See k8s/README.md for the two objects that could not move and why.

selectorLabels — why most files have one

spec.selector cannot be changed after a workload is created. Most services here select on app: <name>, not the chart's app.kubernetes.io/name + instance, so each file records the selector its workload already carries. Without it the chart would emit its own and the apply would be refused.

It is the selector only — the full label set on metadata is still the chart's.

Verifying, before changing anything

# render every file and diff it against the live object
python3 <universe>/charts/app/hack/diff-live.py --context <ctx> --values deploy
  • BREAKING — the apiserver would refuse the apply. Only a workload's spec.selector qualifies; it is immutable.
  • DRIFT — live differs from the file. Someone changed one of them.
  • BENIGN — an intended consequence of moving to the chart.

What is NOT here

Chain CRs (LuxChain, LuxNetwork, LuxIndexer, LuxGateway, LuxMPC) are not services and have their own controllers. They stay in k8s/.

Secrets are not here and never will be. The chart has no way to carry a secret value: kmsSecrets names a path in KMS and the operator syncs it.

lux-system/s3 is deliberately absent. hack/live2values.py generated one from Deployment/s3 and both halves were wrong: that Deployment is a 0-replica husk, and Service/s3 selects app: seaweedfs with a single endpoint on a StatefulSet — so a sync would have repointed it at a selector matching zero pods and taken S3 down for every in-cluster consumer. Correcting it means picking one name for the thing, and renaming a StatefulSet is a recreate against its bound volume. Until that lands, lux-system S3 is visibly outside CD, which is the truthful state; a values file claiming otherwise is worse than an absence.

Why the chart is pinned at 0.1.11 and not 0.1.13

One values file does not render under 0.1.13:

deploy/lux-cloud/price.yaml
    kms: Additional property hostAPI is not allowed
    kms: Additional property credentialsSecret is not allowed

0.1.13 moved KMSSecrets to a ZAP transport: transport + zapAddr replace hostAPI and the per-CR credential, and the rendered object's group changes from secrets.lux.network/v1alpha1 to kms.hanzo.ai/v1. That is a cluster-side change, not a values edit — the lux KMS operator has to serve the new group, and the ApplicationSet's ignoreDifferences still names the old one. The chart's default zapAddr is cloud.hanzo.svc:9653, which is a hanzo-cluster address; Lux's KMS plane is kms.lux.cloud, and what its ZAP endpoint is has not been established.

So the bump is a migration with a cluster prerequisite, and it is not done here. The rest of 0.1.13 is already satisfied: image.digest is present wherever the chart's claim guard requires it, and lux-explorer/lux-faucet no longer carries its version inside image.repository. Under 0.1.11 all 122 files render.

Pins still to move

pin.sh --verify reads 121 pins and 116 agree with their registry. The five that do not are credentials rather than defects:

Pin Why it cannot be read
hanzo/lux-admin registry.digitalocean.com, no DO registry credential
lux-devnet/explore-fe-pars, lux-testnet/explore-fe-pars, lux-mainnet/explore-fe-pars ghcr.io/parsdao/explore answers 403 to a luxfi token

About a hundred services still carry a concrete version with no digest. Those are not ambiguous the way :latest was — the tag names a release — but a registry tag is mutable, which is how zoo/exchange-api came to be pinned at bytes its own version no longer meant. The end state is a digest beside every tag, written by the release that built it. pin.sh <service> <version> does that for any service whose repo dispatches on publish; --freeze is the interim for one that does not.