Files
universe/deploy
zeekayandHanzo Dev 0bf2051754 api.lux.cloud and api.lux.network answer the trading surface
`ghcr.io/luxfi/dex` carries a second binary, `gateway`, and nothing ran it, so
every quote a Lux interface asked for reached nothing. It runs here as `trade`
— not `gateway`, because this namespace already has one in front of the
validators' RPC.

Two routers on one host and the chain's is the more specific: /v1/chain keeps
priority 300 and this takes 200, above the bare-host router in the ingress file
provider that has been answering these paths out of hanzoai/cloud.

The paths are an allowlist. `PathPrefix(/v1)` would be shorter and would publish
/v1/admin/pool/freeze, which the binary authorises on a request header the
caller writes; those handlers stay reachable from inside the cluster and from
nowhere else, and the header is stripped on the way in as well.

96369 is read through the V3 quoter that landed with the AMM today, and read
over the cluster Service rather than back through Cloudflare — same endpoint,
15ms instead of 149ms, and no dependency on the edge that is serving the answer.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 00:18:29 -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.