Files
zeekay 51460827d4 Merge the forge's lux/universe into GitHub's, the one main from here
The two mains shared no history, so this is a per-file three-way merge:
for each file the 65 forge-only commits touched (by patch-id), the base is
the newest version both histories held. 105 files merged or took one side
cleanly, 48 were already identical, and the 45 true conflicts and 31
modify/delete pairs were resolved by what runs on the cluster:

- The forge wins where it is live: explorer 1.5.56 (digest 45db11ff) and
  its archive readers, explore-fe-hanzo's ConfigMap env (AI coin,
  /v1/indexer/hanzo, /v1/chain/hanzo/rpc), exchange-api reading
  luxd-archive, and explore-fe-{lux,zoo}-testnet, pinned to what they run
  (v2.8.5-lux / v2.8.4-lux).
- GitHub wins where it is live: explore-fe-lux v2.8.5-lux, explore-fe-zoo
  v2.8.4-lux, bankd v1.0.18, lux-build's routes.
- The forge's retirement of workloads that run nowhere stands: lux-mainnet
  dex, lux-hb2, lux-market, traffic, wallet-web; the lux-testnet frontends,
  indexer, exchange and gateways; lux-operator. deploy/lux-testnet/luxd.yaml
  stays, since roll_luxd.sh reads it.
- GitHub's deletions stand: lux-finance and lux-financial (static sites
  now), the k8s/ operator manifests, .hanzo/workflows/cicd.yml.
- Paths are /v1/chain/C/rpc where the forge had written /v1/chain/c. All
  four spellings answer; this one is the documented form. The forge's
  /{network}/v1 doc lines are dropped: a network is a host, and the prefix
  404s.

Dry-run against the running objects (server-side, as CD applies), the
merged explorer shows no change where GitHub's showed 152 lines, and no
other live workload moves further from what runs. Every deploy/*/*.yaml
renders with charts/app; price.yaml, which failed on GitHub, now renders.

Claude-Session: https://claude.ai/code/session_01TzirYkGmD88w4GipqivUtV
2026-09-23 20:20: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 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.