The store keys on (path, name) and accepts an empty path, so List and POST have
always handled secrets at the root of an org. GET and DELETE split `{rest...}` on
the last "/" and returned 400 "path and name required" when there wasn't one —
so every root-level secret was permanently unreachable while List cheerfully
advertised it. Against the same deployed v1.12.9:
GET /v1/kms/orgs/bootnode/secrets?path=&env=prod
-> 200 {"names":["DATABASE_URL","API_KEY","APP_SECRET", ...]}
GET /v1/kms/orgs/bootnode/secrets/DATABASE_URL?env=prod
-> 400 {"message":"path and name required"}
Two routes over one store disagreeing about what is addressable.
This is why KMS→k8s secret sync has been 100% dead since 2026-03-17 (54/54
KMSSecret CRs failing on lux-k8s). The bootnode org stores its secrets flat, so
the kms-operator could enumerate them and never fetch one. Auth, org scoping and
the operator's native /v1/kms client were all fine — the fetch could not
express "a secret at the root".
Split on the last "/" when there is one; otherwise the whole rest is the name.
An empty name still 400s (now "name required", which is what it means). DELETE
carried the identical split so it gets the identical guarantee.
TestSecretRoutes_RootLevelSecretIsAddressable pins the round trip: list sees it,
get fetches it, delete removes it, 404 after. Verified red/green — without the
handler change it fails with the exact production error.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
299b230 deleted this in favour of platform.hanzo.ai native builds (hanzo.yml).
The native plane does not serve yet — the platform Deployment is scaled to 0/0
and PLATFORM_BUILD_CALLBACK_TOKEN, the bearer POST /v1/runner requires, is in
neither cluster — so the cutover left kms the one repo in the fleet that could
not build at all, while every sibling kept building on Actions (luxfi/node has
19 active workflows, hanzoai/gateway 3, hanzoai/commerce 1).
The 422 that motivated the cutover, "Actions has been disabled for this user",
is account-scoped rather than repo-scoped: hanzo-dev gets it, zeekay does not.
Push-triggered runs separately stopped firing around 2026-07-24, so builds want
an explicit dispatch regardless.
hanzo.yml stays and now says plainly that it is intent, not the live path. Cut
over when the native plane serves, and fleet-wide rather than one repo at a time.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The ZAP wire has carried OpSecretList (0x0042, {path, env} -> {names}) since it
was written. HTTP had get/put/delete and no list, so the two framings of the
same store disagreed about what you could ask it: a client that wanted to
enumerate had to open a ZAP connection or give up. The hanzo browser extension
gave up and pinned itself to Infisical /api/v3 paths instead.
GET /v1/kms/orgs/{org}/secrets?path=&env= -> {"names": [...]}
Same handler shape and same envelope the ZAP op returns — one store, two
framings, now the same question set.
The pattern has no trailing segment, so ServeMux routes the bare collection here
and anything below it to the existing {rest...} handler. That split is the whole
risk in this change: invert it and list swallows every get, or get 400s on the
bare collection for want of a slash to split. TestSecretRoutes_ListAndGetDoNotShadow
asserts both directions, plus that an empty result is [] and never null.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two homes existed for one job — reconciling KMSSecret CRs into k8s Secrets:
cmd/kms-operator (252 lines) -> ghcr.io/luxfi/kms-operator
hanzoai/kms-operator (own repo) -> ghcr.io/hanzoai/kms-operator
hanzoai/kms-operator is the one that actually runs, on BOTH clusters
(lux-k8s kms-operator-system/kms-operator-controller-manager and hanzo-k8s
kms-operator-canonical). This repo's own docs already say so:
internal/documents/architecture.md calls hanzoai/kms-operator canonical and the
luxfi name "older"; index.md flags the remaining luxfi pins as drift. The only
thing still pinning ghcr.io/luxfi/kms-operator is lux-devnet/kms-zap-operator,
scaled 0/0.
So this was the dead duplicate. Delete it rather than keep fixing it — I had
just repaired its vendor drift in v1.12.8, which fixed a build for a component
that should not exist. Deleting beats fixing a second way to do one thing.
Also drop NOTICE. It carried the MIT attribution for Infisical-derived code —
required while that code was present, and it was: the vendored Infisical
console. a6a933d removed it. Nothing derived remains (0 TS/JS files, 0
Infisical references across all 60 Go files), so the attribution no longer
applies. The empty frontend/ directories go too.
The KMS service itself is untouched: cmd/kms builds and pkg/keys + cmd/kms
tests pass.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
GitHub answers a workflow dispatch on this repo with 422 "Actions has been
disabled for this user", so pushes to main produced no build at all — the last
run is still the commit before the console removal, and the deployed image
drifted behind main without anything reporting a failure.
hanzo.yml hands the build to platform.hanzo.ai, which schedules it onto the
self-hosted arcd pools over the native fabric and pushes to GHCR. Same path
luxfi/node uses. .github/workflows/build-lux-kms.yml is retired rather than
left as a second, broken way to produce the same image.
No deploy block: the kms Deployment is kustomize-managed with no operator CR,
so there is nothing for a Service-CR rollout to target.
VERSION 1.12.6 -> 1.12.8. The file had fallen behind the deployed tag v1.12.7,
so a tag build would have gone backwards.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
a6a933d ("kms is the Go service on /v1/kms — delete the Infisical console")
removed Dockerfile.server and Dockerfile.web but left .github/workflows/
untouched. build-server-self.yml still passes `file: Dockerfile.server`, so
every run since fails immediately:
ERROR: failed to solve: failed to read dockerfile:
open Dockerfile.server: no such file or directory
Delete the workflow rather than repoint it. It existed to build a second,
"self-contained" variant of the same server into ghcr.io/luxfi/kms:server —
a duplicate path for one job. The main Dockerfile (Build KMS) already builds
the server, and the :server image is consumed by nothing: zero pods reference
it on either lux-k8s or hanzo-k8s, and the only remaining mentions are this
workflow and a historical note in hanzo/kms/DEPRECATED.md.
Leaves one Dockerfile and one server build.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
ghcr.io/luxfi/kms-operator has had no new image since v1.12.3. The Build KMS
Operator workflow failed on every tag since (v1.12.4 … v1.12.7) while its
sibling Build KMS succeeded on the same tag, so the tags looked published.
Dockerfile.operator:27 builds with -mod=vendor, and vendor/ had drifted from
go.mod in both directions:
- go.opentelemetry.io/otel/{metric,trace}: in vendor/modules.txt, not in go.mod
- github.com/luxfi/{crypto,geth,ids,keys,zap,address,age,cache,constants,
container,formatting,math,go-bip32,go-bip39}: in go.mod, not marked explicit
`go mod vendor` alone could not fix it — it aborted first on go.sum entries
missing for the k8s packages cmd/kms-operator imports (k8s.io/api/core/v1,
k8s.io/apimachinery/..., k8s.io/client-go/{dynamic,kubernetes,rest}). So:
go mod tidy (adds them, drops now-unused indirects) then go mod vendor.
Verified by stash control:
operator, -mod=vendor: rc=1 -> rc=0 ("inconsistent vendoring" 1 -> 0)
server, -mod=vendor: rc=1 -> rc=1 (unchanged)
The server's vendor build was already broken and stays broken, for an unrelated
reason: blst.h has never been vendored (confirmed absent from HEAD's tree, so it
could not have worked before either) — go mod vendor does not copy cgo headers
that live outside the package dir. It is not a regression and does not affect
CI: the server's Dockerfile builds with GOFLAGS=-mod=mod after `rm -f go.sum &&
go mod download`, i.e. from the module cache, never from vendor/. The operator
is the only -mod=vendor consumer, which is exactly why it was the only failure.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Removing the embedded console stopped the SPA answering every unmatched path,
but the mux could still emit HTML on its own: http.ServeMux writes an HTML body
for the implicit trailing-slash redirect, http.Error writes text/plain, and the
default unmatched-route handler writes "404 page not found".
jsonOnly wraps the mux so every response carries application/json and a JSON
body regardless of what the handler underneath wrote. Status and Location are
untouched, so redirects still redirect — they just stop explaining themselves in
HTML. "/" is registered as a JSON 404 carrying the requested path, so a wrong
URL says so in a form a caller can act on.
This is the failure that let genesis call /api/v3/secrets/raw against this host
and read the console page back instead of an error. Four tests pin it: unmatched
paths are JSON 404s, redirect bodies contain no HTML but keep Location, plain
text errors become JSON with the message preserved, and JSON handlers pass
through byte for byte.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This repo carried two KMS implementations. cmd/kms is a Go service serving
/v1/kms/orgs/{org}/secrets/{path}/{name} out of ZapDB behind IAM-signed
bearers, and its own source calls that "the ONLY HTTP way to read or write
secrets". Alongside it sat frontend/ — 3,083 files of the Infisical console,
calling /api/v1/{dynamic-secrets,secret-scanning,projects}. Two systems, one
name. LLM.md already listed frontend/ as "Legacy — Old React dashboard" and
claimed "NO Node, NO Infisical"; the tree said otherwise.
Deleted frontend/, the go:embed of its build output, registerWebUI, and the
/v1/admin/config endpoint that existed only to bootstrap that SPA. The three
Dockerfiles collapse to one: the server-only build, which the frontend-building
variants existed to work around. Makefile loses build-ui/copy-ui. CI already
pointed at `Dockerfile` and so needs no change.
This also removes a trap. The embedded SPA answered every unmatched path with
HTML, so a wrong URL looked like a 200 instead of a 404 — which is exactly how
genesis called Infisical /api/v3/secrets/raw against this host for however long
and got the console page back instead of an error. The API now 404s honestly.
8,407 tracked files -> 5,320. Every route is /v1/kms/* plus /health{,z}.
Build clean, cmd/kms and root package tests pass.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
GenerateValidatorKeys validated req.Threshold (>=2, parties >= threshold) and
then never sent it — mpc.KeygenRequest has no threshold field, because the MPC
ring's own --threshold decides the CGGMP21 polynomial degree. The request was
advisory; the result was trusted blind.
That was not a theoretical gap. mpcd's KeygenResult carried neither threshold
nor participants, so `Threshold: blsResult.Threshold` and
`Parties: len(blsResult.Participants)` both evaluated to zero. Every validator
key set on record reads 0-of-0 while its caller requested 3-of-5 and passed
validation. The unit tests missed it because the mock server returned
threshold:3 and five participants — it told the truth the real ring never did.
Add verifyThreshold: one function, called for both the BLS and Corona keys.
It refuses a mismatch in either direction, and refuses just as firmly when the
ring declines to state what it did — silently trusting an unstated threshold is
the exact failure being guarded. Nothing is written to the store on refusal, so
a rejected key leaves behind no record asserting a property never established.
DEPLOY ORDER: requires mpcd >= v1.17.15 (luxfi/mpc 81be6b9), which is what
began reporting these fields. Against an older ring every keygen now fails
closed with an explicit "upgrade the MPC ring" error instead of silently
recording 0-of-0. Roll the MPC quorums first.
Tests: 5 refusal cases (unreported, weaker, degree-0, fewer parties, stronger)
each asserting nothing is stored, plus a positive control proving a matching
3-of-5 is accepted and recorded as 3-of-5.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Dockerfile.server used plain 'go build' (vendor mode) + a single pre-COPY
'go mod download'. Two breakages: (1) the committed vendor/ lags go.mod
(luxfi/vm v1.2.3 in modules.txt vs v1.3.1 in go.mod) -> 'inconsistent
vendoring'; (2) COPY . . re-clobbers any regenerated go.sum with the stale
committed one, so a force-republished luxfi tag aborts the build on a
checksum mismatch.
Mirror the proven-green main Dockerfile: GOSUMDB=off + rm -f go.sum &&
go mod download before AND after COPY . ., and GOFLAGS=-mod=mod on build
so it resolves from the module cache instead of the inconsistent vendor
tree. Regenerate-not-bypass: integrity still enforced against go.sum.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The builder already regenerated go.sum under GOSUMDB=off before copying source
(to survive luxfi's periodic tag re-publishes), but `COPY . .` re-introduced the
committed go.sum and silently reverted that regeneration. When luxfi/vm@v1.3.1 was
force-moved, the committed go.sum (h1:rLCby...) stopped matching the bits the CI
runner fetches (h1:U2Bv7...), so `go build -mod=mod` aborted with a checksum
mismatch — the 2026-07 "Build KMS" red that blocks shipping the v1.12.5 env-leak
fix. Regenerate go.sum from the primed cache after COPY so the build verifies
against what it actually fetched. Integrity stays enforced against the regenerated
go.sum (regenerate-not-bypass).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The route was registered on the bare mux with NO auth middleware and did
os.Getenv(name), returning the raw value. A caller reaching :8080 past the
network boundary (NetworkPolicy gap, port-forward, pod compromise, SSRF) could
`curl .../v1/kms/secrets/KMS_MASTER_KEY_B64` with no Authorization header and
exfiltrate the root REK protecting every per-secret DEK — plus MPC_TOKEN,
KMS_ENCRYPTION_KEY_B64, and (k8s/ variant) the S3 backup keys. On the live
lux-kms-go/kms statefulset KMS_MASTER_KEY_B64 is populated, so this was live
master-key exposure gated only by the network → CRITICAL.
Deleted, not gated: zero callers (kms-operator + kms client read via the
org-scoped path/ZAP; process env is never a secret source). The three
org-scoped secret routes are extracted into registerSecretRoutes so the exact
exposed route set is testable in isolation. Secrets now flow ONLY through
/v1/kms/orgs/{org}/secrets/* (requireOrgJWT) and the ZAP wire.
Regression TestSecretRoutes_NoEnvVarLeak: unauth -> 404, admin -> 404, the
process-env canary is never in the body. Corrected the stale kms.go doc
comment that referenced the route. Full cmd/kms suite green (CGO_ENABLED=0).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The /sign response was {signature, r, s} — no v, S not low-S normalized, and
signature sometimes empty. A high-S signature is rejected by luxfi/evm (EIP-2)
and a missing v forces a recovery-id guess (wrong guess => wrong address).
mpcd now emits EIP-2 low-S R/S, the recovery id V, and the canonical 65-byte
r‖s‖v (via the wired SigEthereum helper). Carry these through:
- pkg/mpc SignResult: parse V from the wire.
- pkg/keys SignResponse + SignWithBLS/SignWithCorona: propagate V. For
secp256k1 Signature is now the ecrecover-ready r‖s‖v; for ed25519 it stays the
64-byte blob with empty V. Callers build EVM tx v as 27+V (legacy) or
chainID*2+35+V (EIP-155). No empty signature field; r/s/v documented.
Tests: TestSignWithBLS asserts v propagates. End-to-end EVM validity proven in
threshold (SigEthereum: 256 random + high-S recover to signer; live devnet
chainID 31337 tx mined status=success).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The /v1/kms/keys/* routes (keygen, sign, rotate, metadata reads) were gated
ONLY by requireMPC — no caller-identity check. The routes were declared
"unprotected at the HTTP layer — auth enforced by Gateway/NetworkPolicy". Any
caller reaching :8080 directly (NetworkPolicy misconfig, pod compromise,
port-forward, SSRF) could sign arbitrary digests with any custodied key, or
keygen/rotate at will. Trusting a Gateway-injected role header is not enough:
a direct caller forges the header; only an IAM signature cannot be forged.
Add requireKeyAuth (orgJWTAuth): verifies the IAM-signed JWT HERE against
JWKS (sig + issuer + expiry) and requires the kms-admin role (superadmin is
break-glass) — the same write authority the zapserver /v1/sdk sign op and the
mpc KMS handlers already enforce. Fail closed at every step: nil auth => 503,
missing/malformed/bad token => 401, authenticated-but-no-role => 403. This is
defense in depth BEHIND the NetworkPolicy, never a replacement for it. The
/v1/kms/status health probe stays open (no key material).
Tests (SDKROOT="$(xcrun --show-sdk-path)" go test ./cmd/kms):
requireKeyAuth: missing bearer 401, malformed 401, nil-auth 503 (fail
closed), valid-token-without-role 403, kms-admin 200, superadmin 200.
failopen suite updated to authenticate (kms-admin) so it still pins the
requireMPC 503/recovery path behind the new gate. Full cmd/kms: ok.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The ecosystem re-tag re-published luxfi/keys@v1.1.0 and luxfi/pq@v1.0.3
under fixed versions (proxy now serves bits that the immutable
sum.golang.org rejects), and wiped geth v1.16.x. The builder's plain
'go mod download' against the committed go.sum + default sumdb therefore
failed ('checksum mismatch'). Adopt luxfi/mpc's proven builder pattern:
GOSUMDB=off + rm+regenerate go.sum from the current proxy/authed-git
source (integrity still enforced against go.sum). Commit a go.sum carrying
the current proxy hashes for local dev. Pairs with the go.mod geth
v1.17.12 bump.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
luxfi/geth wiped its v1.16.x line (v1.16.98 gone) in the ecosystem
re-tag, breaking 'go mod download' at Dockerfile:27. Move to the coherent
set crypto v1.19.27 -> geth v1.17.12 (both proxy-resolvable), regenerate
go.sum against the public proxy (matches the Dockerfile's proxy fetch),
and re-vendor so the in-tree vendor/ stays consistent. Pairs with the
already-landed KMS<->MPC ZAP wire fix (7377ad6) so a tagged release both
builds and signs against mpcd.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The KMS ZapClient and the mpcd KMS-facing ZAP server (luxfi/mpc
pkg/api/zap_kms_server.go) had drifted out of lockstep despite the
"MUST stay in lockstep" comment — each was only unit-tested against its
own stub, never against the other's real bytes. Three drifts:
1. Sign request: KMS sent {key_type, wallet_id, message}; mpcd requires
{vault_id, wallet_id, payload} and hard-fails "vault_id and wallet_id
required". SignRequest now carries VaultID (Manager supplies it from
MPC_VAULT_ID) and Payload (json "payload").
2. Keygen response: mpcd returns snake_case (wallet_id/ecdsa_pub_key/
evm_address); KMS KeygenResult was camelCase → decoded to an empty
struct. Tags realigned to snake_case (the ZAP path is the only live
decode; the REST Client is unused).
3. False-green: ZapClient.call() json-decoded the daemon's {"error":...}
frame into a zero-value SignResult and returned it with nil error —
the KMS looked MPC-backed but returned an empty signature. call() now
surfaces a non-empty top-level "error" as a real error (fail closed).
Adds pkg/mpc/wire_contract_test.go — a cross-repo contract test that
transcribes the mpcd request/response shapes and fails CI if the KMS
side drifts again (the guard that was missing). Existing tests updated
to the corrected snake_case wire.
This is a library fix in luxfi/kms — it fixes the wire for all three
orgs (Lux/Zoo/Hanzo) once. ed25519/Corona keygen over this wire remains
a follow-up (mpcd's KMS-ZAP keygen carries no key_type/protocol).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
KMS_EXPECTED_ISSUER now takes a comma-separated allowlist so one KMS can
serve several white-label brands that share IAM signing keys. The Lux-brand
KMS (kms.lux.network) must accept lux.id tokens — lux.cloud users carry
iss=https://lux.id — while still honoring hanzo.id. JWKS is unchanged
(fetched in-cluster; both brands sign with the same keys), so this is a pure
issuer-string allowlist: parse+dedupe, then validate membership after the
signature/expiry check. Backward compatible: a single-value
KMS_EXPECTED_ISSUER behaves exactly as before.
Deploy: KMS_EXPECTED_ISSUER=https://hanzo.id,https://lux.id on the Lux KMS.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
One canonical path for every Lux-derived service (luxd, netrunner, cli,
descending L1 bootstrap) to resolve the bootstrap mnemonic: MNEMONIC env
wins for local/CI; KMS over native ZAP is the production fallback. Lives
in luxfi/kms (not luxfi/keys) to keep the keys->kms edge from closing an
import cycle. Real guard test covers env-precedence (no KMS dial when env
set), BIP-39 validation reject, KMS-fake valid/empty paths, and the
SplitSecretPath addressing convention.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
frontend CSP no longer needs to allow *.stripe.com (Infisical-era billing
UI we don't use). Drop all stripe.com sources + the stripe-only child-src.
Rename the Stripe-flavored example secret in zapserver/envelope tests to a
neutral api-key/secret-value fixture. No Go code paths change.
After the keys@v1.1.0 go.sum fix (1889442), the Build KMS run for
v1.9.14 surfaced a SECOND re-tagged module at 'go mod download':
luxfi/age@v1.5.0 was force-moved the same way (the 'fa86744 vendor:
sync luxfi/keys + luxfi/age docs' lineage), so CI's direct-from-GitHub
fetch computed h1:zC/Fw/… while go.sum pinned the stale
h1:G69HbSV4… → checksum mismatch.
Updated the one age h1 line to the authoritative current hash
(verified from a pristine GOMODCACHE; the /go.mod hash was unchanged).
A full pristine 'go mod download' of the entire module graph (CI
fidelity: isolated cache, GOPROXY=direct, GOWORK=off) is now CLEAN —
no remaining checksum mismatches. age is not compiled into the
cmd/kms server binary (transitive verify-only dep).
Build (Build KMS / Dockerfile, -mod=mod) failed 17 days at 'go mod
download': luxfi/keys@v1.1.0 was re-tagged (force-moved to a different
commit) after kms's go.sum was written, so CI's direct-from-GitHub
fetch (GOPRIVATE) computed h1:D3s754vT… while go.sum pinned the stale
h1:a4UkVVg6… → 'SECURITY ERROR: checksum mismatch'. Regenerated the one
go.sum line to the authoritative current hash (no security bypass).
The keys↔kms 'cycle' (keys@v1.1.0 → kms@v1.9.12, a tag that does not
exist) does NOT block this build: the package graph is acyclic
(go list -deps ./pkg/zapclient has zero luxfi/keys; pkg/envelope is
interface-decoupled, keys appears only in its _test files), and MVS
resolves the kms self-module from the local tree, never fetching the
phantom v1.9.12. The production server binary (cmd/kms) and operator
(cmd/kms-operator) compile ZERO external luxfi/keys (cmd/kms uses the
in-repo pkg/keys; only the cmd/smoke-replay dev tool pulls external
keys) — so no keys content enters the shipped images.
org sub-scope auth: the lux-operator queries KMS with org=lux-infra
(its Infisical projectSlug) but the lux-kms token's org claim is 'lux'
→ strict equality returned 403. orgAuthorizes() now admits a requested
org that equals the token org OR is a hyphen-delimited sub-scope of it
(lux authorizes lux-infra/lux-mainnet/…; the '-' boundary keeps lux
from leaking into luxx-infra). One function, one call site.
Tests: cmd/kms ok (14 TestRequireOrgJWT incl. 2 new sub-scope cases),
pkg/{envelope,zapclient,zapserver} ok.
zapclient imports github.com/luxfi/kms/pkg/{envelope,zap}, which are the
module's own packages. -mod=vendor only vendors external deps, so these
must be present in the Docker build context as source. The selective COPY
missed them, so the build failed:
pkg/zapclient/client.go:34:2: cannot find module providing package
github.com/luxfi/kms/pkg/envelope: import lookup disabled by -mod=vendor
COPY both packages; also add them to the workflow paths filter so the
operator image rebuilds when they change.
The deployed kms-operator (ghcr.io/luxfi/kms-operator:0d446b0) is the legacy
Infisical-flavor HTTP operator: it POSTs /api/v1/auth/universal-auth/login,
which the Go KMS server does NOT expose, so every KMSSecret sync fails with
CallUniversalAuthLogin 404 (e.g. lux-devnet/luxd-staking-kms-sync). The fix —
cmd/kms-operator, which reconciles over the native luxfi/zap binary transport
on port 9999 (no HTTP auth round-trip) — already exists in source but had no
CI to ship it. Add a self-contained build for Dockerfile.operator (same
pattern as build-lux-kms.yml), publishing ghcr.io/luxfi/kms-operator.
cloudbuild.kms.yaml built and pushed lux/kms to 's GAR
(us-docker.pkg.dev/-devnet) — a downstream deployment artifact
that doesn't belong in lux OSS (lux builds to ghcr.io/luxfi via arcd).
Remove it; genericize the / strings in zapclient +
secrets doc comments.
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.