One label per runner, spelled <os>-<arch>. The aliases these jobs named
were removed, and a job naming a label no runner carries waits instead of
failing.
Co-authored-by: Hanzo Dev <[email protected]>
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.
Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
Co-authored-by: Hanzo Dev <[email protected]>
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.
Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.
Comments naming a face we no longer use are deleted, not reworded.
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.
lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.
Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.
Co-authored-by: Hanzo Dev <[email protected]>
These workflows already ran green on git.hanzo.ai — they were among the few
that had been corrected to authenticate to ghcr.io with GHCR_USER/GHCR_TOKEN
rather than secrets.GITHUB_TOKEN, which on this forge is a git.hanzo.ai token
that ghcr.io answers "denied: denied".
What was wrong was the location. .github/workflows is read by both the forge
and GitHub, and the luxfi GitHub org has zero registered runners, so every one
of these also sat queued on GitHub until it was cancelled. One build, two
schedulers, one of which can never finish. Moving to .hanzo/workflows leaves
exactly one.
Runner label normalised to lux-build-linux-amd64. lux-build, lux-build-amd64
and [self-hosted, linux, amd64] are three names the git-runner answers to for
the same pool; one name for one thing.
Co-authored-by: Hanzo Dev <[email protected]>
`go mod download` failed with a checksum mismatch on hanzoai/[email protected].5.
Same cause already documented in the Dockerfile and release.yml -- commerce went
private AFTER it was notarised, so the public proxy still serves the pre-privatisation
zip while go.sum records the direct hash the tag points at today.
Both other places were fixed; ci.yml was missed, so it kept failing alone.
go.sum is CORRECT and must not be "fixed" to the proxy value -- sum.golang.org
holds the proxy hash, so a go.sum-vs-sumdb audit points the wrong way for this
module. Measured with GOPRIVATE set: `go mod download` exits 0 against the
committed go.sum.
Co-authored-by: Hanzo Dev <[email protected]>
`go mod download` failed inside the image build:
verifying github.com/hanzoai/[email protected].5: checksum mismatch
SECURITY ERROR
Three faults compounded. github.com/hanzoai/* is private, but the Dockerfile
said "No GOPRIVATE -- public modules resolve via the immutable public proxy",
which stopped being true when that repo went private; Go therefore asked the
proxy, which still serves the pre-privatisation zip. The credential arrived as
`ARG GITHUB_TOKEN` and the workflow passed neither build-args nor secrets, so it
expanded to `https://@github.com/` and authenticated nothing. And an ARG is
recorded in image history, so had a token ever been supplied that way it would
have shipped inside the published image.
Now: GOPRIVATE names hanzoai only, and the token arrives as a mounted secret
scoped to the one RUN -- the same `--mount=type=secret,id=gh_token` shape used
across the estate. luxfi/* stays public on purpose, so the proxy keeps it pinned
to bytes the checksum database signed.
go.sum is untouched: it holds the direct hash, which is the correct one here.
Co-authored-by: Hanzo Dev <[email protected]>
The release died in goreleaser`s `go mod tidy` hook:
verifying github.com/hanzoai/[email protected].5: checksum mismatch
downloaded: h1:RS6pjCKOAjigi…
go.sum: h1:DjGiR7NSOZYD+…
SECURITY ERROR
github.com/hanzoai/* is private, but nothing here set GOPRIVATE, so Go resolved
it through the public proxy -- which still serves the bits from before the repo
went private, a different zip than the tag points at today. go.sum records the
direct hash, so the two could never agree.
⚠️ go.sum is the CORRECT side and is left untouched. I checked the other way
first: with GOPRIVATE set, `go mod download` exits 0 against the committed
go.sum. sum.golang.org holds the proxy value, so an audit of go.sum against the
checksum database points the wrong way for a module that went private AFTER it
was notarised. The polarity has to be measured per module, not assumed -- the
opposite case (go.sum wrong, sumdb right) was true for luxfi/pq in dex an hour
ago.
A direct fetch of a private module needs a real credential, so GHCR_TOKEN is
wired in as a git url rewrite. It is also now what goreleaser publishes with:
`secrets.GITHUB_TOKEN` is git.hanzo.ai`s token on a self-hosted pool and
github.com has never seen it.
luxfi/* is deliberately not in GOPRIVATE -- it is public, and resolving it
through the proxy is what keeps it pinned to bytes the checksum database signed.
Co-authored-by: Hanzo Dev <[email protected]>
Go's base64 decoders are lenient in two independent ways, and both JWT
verifiers used them raw, so one credential validated under several spellings.
The unused low bits of the final quantum are ignored: an HMAC-SHA256
signature is 32 bytes in 43 base64url characters, leaving 2 slack bits and
so 4 accepted strings per admin token; an RSA-2048 signature is 256 bytes in
342 characters, leaving 4 slack bits and so 16 per IAM token. Separately,
\r and \n are skipped wherever they appear, which Encoding.Strict() does not
close -- so Strict alone would not have been a fix.
Every string-keyed structure over a credential silently splits under that: a
denylist misses the spellings it was not handed, an audit log reads one actor
as several, a rate limiter multiplies the budget by the class size.
pkg/token holds the one canonical encoding. Decode round-trips rather than
enumerating failure modes -- a segment is canonical exactly when it is the
encoding of the bytes it decodes to -- and rejects rather than normalizes.
token.ID is the hash to key revocation, audit and rate limiting on, so the
identity is never the submitted string.
Both verifiers now decode every segment through it. JWKS n/e stays lenient on
purpose: public key material from the trusted IAM endpoint, where every
spelling is the same key.
Also in pkg/admin: sign returns raw MAC bytes instead of text, dropping an
encode-then-decode round trip in the verify path; the header is now decoded
and required to declare HS256, closing alg confusion under future refactor;
and a comment referencing a long-deleted hashPassword is gone.
Tests: exactly one spelling authenticates on both paths; revocation keyed on
token.ID holds across all four; CR/LF, padding, standard alphabet and empty
segments rejected. TestValidateToken_TamperedSignature substituted a fixed
'X' that was a no-op whenever the token already ended in one -- a 1-in-64
flake that this bug made a 1-in-16 one -- now derived from the token.
LLM.md: admin auth section corrected (bcrypt cost 12, not SHA-256+salt; no
ADMIN_* env vars exist; middleware strips those headers rather than setting
them; package is not mounted).
Co-authored-by: Hanzo Dev <[email protected]>
Every job in this file runs on a lux self-hosted pool, so GitHub is not the
executor -- but the login presented `secrets.GITHUB_TOKEN`, which git.hanzo.ai
mints and ghcr.io has never seen:
Error response from daemon: Get "https://ghcr.io/v2/": denied: denied
The login step failed and every job after it skipped, so the image was never
published. The pair is GitHub`s own idiom and is correct there; it is simply
not a credential anywhere else.
orgs/luxfi provisions exactly GHCR_USER + GHCR_TOKEN. Named directly rather
than through a `||` chain: a missing org secret evaluates to "", so a fallback
silently swaps credentials and the log never says which one was used -- that is
what made the same failure in luxfi/trader read as a registry problem.
Co-authored-by: Hanzo Dev <[email protected]>
Currency bump: golang builder images -> 1.26.5 (latest stable).
Adds ENV GOTOOLCHAIN=auto to each Go builder stage. The official golang
images set GOTOOLCHAIN=local, so a builder image older than the module's
go directive is a hard mid-build failure. The pin stays the hermetic fast
path; auto is the safety net for the next go.mod bump.
No go.mod go directive was changed. Builder images move; directives stay.
Co-authored-by: Hanzo Dev <[email protected]>
Remove the optional github.com/luxfi/compliance/pkg/jube provider from the
broker compliance layer. AML screening keeps working via the OFAC SDN list
and the ScamSniffer database; the native github.com/luxfi/aml engine (amld)
is the deeper risk-scoring backend. Removed the jube import, the jubeClient
field/option, and every jube-guarded block; docs updated to reflect the
native engine.
Co-authored-by: Hanzo Dev <[email protected]>
The Dockerfile set GOPRIVATE=luxfi/*,hanzoai/* which routes module
fetches 'direct' (git) and bypasses sum.golang.org. luxfi/hanzoai Go
modules are all PUBLIC; after a tag was force-republished, the git tag
content diverged from the immutable proxy/sumdb hash — so 'go mod
download' fetched bits that no longer matched the canonical, sumdb-correct
go.sum, failing image builds with SECURITY ERROR checksum mismatch.
Dropping GOPRIVATE resolves these modules through the default public
proxy + sumdb, yielding the canonical immutable hash that matches go.sum
and that a re-publish can no longer break. GITHUB_TOKEN insteadOf stays
for the direct fallback. go.sum re-tidied against the public sumdb is
byte-identical (already canonical); only the Dockerfile changes.
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.
New pkg/pretrade implementing Gap G-37 (Stage 6.5): synchronous gate
at order-entry that fetches state from cap-table, transfer, AML, KYC,
and on-chain ERC-3643 compliance providers and decides allow / deny /
escalate. Each predicate is a composable internal function; the
Decision carries every actionable reason in a single round.
Wired into the broker order path via Wrap[Req, Resp] generic
middleware so existing router signatures stay untouched (additive,
forwards-only). GateError exposes the Decision to callers that surface
required actions to the investor.
Audit is delegated through AuditSink; canonical AuditLogSink bridges
to broker/pkg/audit. PII (ClientIP) is masked to /24 / /48 in the
audit row.
Test coverage: happy path, every hard-deny predicate (kyc_not_verified,
kyc_stale, not_accredited, accreditation_stale_{income,third_party},
suitability_{missing,stale}, aml_not_clean, open_sar,
ofac_screen_stale, lockup_active, rule144_holding_period,
country_restricted, max_holders, transfer_restricted,
chain_compliance_refused), every escalate predicate
(suitability_ambiguous, kyc_near_expiry, accreditation_near_expiry),
upstream-provider error propagation, input validation, max-holder
sell-side exemption, max-holder existing-holder exemption, PII
redaction. 41 tests passing under -race.
Source-of-design: Lux-Prior-IP
Source-ref: f7a8d7a
Clean-Team-Member: dev-agent (P0 G-37 implementation)
Signed-off-by: dev <[email protected]>
Builds on luxfi-org ARC runners per-arch (lux-build-linux-amd64,
lux-build-linux-arm64), pushes by-digest, then assembles a multi-arch
manifest tagged with the semver release tag plus :latest. No QEMU, no
cross-org workflow_call.
goreleaser builds the six-platform binary matrix (linux/darwin/windows ×
amd64/arm64) with SBOMs and checksums on `v*` tag push. docker.yml is
pinned to the lux-build-linux-{amd64,arm64} ARC runners (native, no
QEMU) and delegates to the canonical hanzoai/.github reusable workflow.
release.yml triggers on `v*` tags and runs on the lux-build self-hosted
scale-set.
ListAssets now fetches Alpaca FI treasuries + corporates alongside
equities and crypto. Maps CUSIP as symbol, bond_status as tradable
filter. Corporates 403 (not subscribed) silently skipped.
ATS registerMarketsFromBroker picks them up automatically.
go mod tidy was not run after replace directives were removed.
CI failed because go.sum lacked entries for luxfi/compliance v0.2.2,
luxfi/zap v0.2.0, and their transitive dependencies.
- New pkg/provider/httpauth/ with Authenticator interface and 3 impls:
* Basic (legacy HTTP-Basic for Kraken, old Alpaca, etc.)
* ClientSecret (OAuth2 client_secret_post, e.g. Alpaca's Client Secret tier)
* JWT (RFC 7523 private_key_jwt, e.g. Alpaca's JWT P-256 tier)
- alpaca_omnisub: Config gains ClientID + PrivateKeyPEM; do() unifies
auth via Authenticator — one codepath covers Basic OR JWT Bearer.
- Alpaca quirk captured: omit `kid` from JWT header (authx rejects it with
invalid_request even though RFC 7523 permits).
- Token URL auto-selects authx.sandbox vs authx prod by base URL.
- Token cache with 60s safety refresh window; 15-min Alpaca TTL.
- Verified live against both devnet (CKVH...) and testnet (CKZR...)
client credentials: token exchange 200, live /v1/clock, /v1/accounts,
and real equity orders placed under Bearer auth.
Sibling to alpaca provider. Runs in parallel with standalone Alpaca using
independent ALPACA_OMNISUB_* env vars. Full Provider interface plus
per-sub positions, omnibus-routed transfers, batch journals + reverse-batch
for trade reversals, omnibus snapshot for reconciliation. 19 tests.
Six new venue adapters for CH, AE, EU jurisdictions. Each implements
provider.Provider with all methods returning ErrNotImplemented — enough
to register, dispatch, and show up in superadmin.
- sdx: SIX Digital Exchange (Switzerland, FINMA)
- adgm: ADGM RegS venues (Abu Dhabi, FSRA)
- securrency: DFSA-licensed digital securities (Dubai DIFC)
- cssf: Luxembourg DLT securities rail (CSSF)
- euronext: Pan-EU exchange (Paris, Amsterdam, Brussels, Lisbon, Dublin, Oslo)
- xetra: Deutsche Boerse / Xetra (Germany, BaFin)
Replace JSON-in-ZAP envelopes with typed ZAP structs at fixed offsets.
Every request and response has a documented schema in schema.go:
Requests Responses
────────── ───────────
SymbolReq SnapshotResp (72 bytes fixed)
SymbolsReq AssetResp (64 bytes fixed)
ClassReq OrderResp (128 bytes fixed)
BarsReq EmptyResp (8 bytes fixed)
AccountReq ErrorResp (16 bytes fixed)
AccountOrderReq
CreateOrderReq
Flags header now carries opcode (upper byte) + status (lower byte, with
StatusOK=0, StatusErr=1). RPCError type surfaces server-side errors
transparently.
Strings use ZAP text fields (4-byte offset + 4-byte length, zero-copy
read). Floats use IEEE-754 binary64. Timestamps are int64 Unix nanos.
Multi-symbol requests pack symbols as CSV in a single text field.
12 tests passing — schema round-trips for every type plus provider-level
ZAP roundtrips for GetSnapshot, GetAsset, GetOrder, CreateOrder,
CancelOrder, and error propagation.
Single broker provider for the on-chain DEX precompile (luxfi/dex). Works
against any chain shipping the DEX precompile: Lux mainnet, Lux subnets,
<tenant> L1. Uses luxfi/zap (zero-copy binary RPC) instead of HTTP/JSON-RPC
or gRPC.
Wire format follows the ledger replication pattern: opcode in flags upper
8 bits, JSON payload in field 0. Opcodes cover assets, snapshots, bars,
quotes, trades, orderbook, and full order lifecycle (create/get/list/cancel).
Smart order router auto-registers when LX_DEX_ADDR is set or
ENVIRONMENT=local. No env var → no registration → router transparently
falls back to external venues.
Tests cover real ZAP roundtrips (TCP listener + client, opcode dispatch,
JSON-in-ZAP envelope encoding/decoding). 5 tests passing.
- checkAudience now rejects tokens without aud when BROKER_JWT_AUDIENCE is set
- credentials migration adds key_hash column (fixes SaveCredential on fresh PG)
- Remove stale "shared between REST and gRPC" comment from main.go
When ENVIRONMENT=local or ENABLE_PUBLIC_DATA is set, register Kraken
and Gemini without API keys. Their public endpoints provide real-time
crypto prices without authentication.
Asset struct: add overnight_tradable, overnight_halted,
fractional_eh_enabled, min_order_size, price_increment,
min_trade_increment — parsed from Alpaca assets API.
CreateOrder: add pre-flight validation:
- Notional max 2 decimal places, qty max 9 decimal places
- Crypto: TIF must be gtc or ioc (rejects day)
- Crypto: max $200k notional, min $1 buy notional
- Crypto: min qty 0.000000002
CreateOrderRequest: add AssetClass field for asset-class-aware
validation without symbol heuristics.
Addresses Alpaca Tech Sign Off items across all 5 checklists.
Org IS the role:
- built-in org = full admin access to all compliance modules
- Any other org = self-service only (sessions, applications, kyc)
Removed: RequirePermission, HasRole, X-User-Roles, X-User-IsAdmin
Added: requireOrgAccess (checks X-Org-Id against adminOrg)
Added: localnet environment config
- Strip 9 identity headers at start of every request (prevents injection via direct pod access)
- Remove X-Account-Id fast path and fallback headers from frontend handlers (fixes IDOR)
- Add gRPC auth interceptor with IAM JWKS validation
- Fix RBAC role splitting (comma-separated X-User-Roles)
- Move webhook endpoint outside auth middleware
- Fatal on empty SSN_HMAC_KEY in production
- Remove dead admin store code
- Remove duplicate CORS handler from compliance router
- Fix writeErr JSON injection (use json.Marshal)
- Propagate isAdmin + roles claims from JWT
Middleware now validates Bearer tokens against IAM JWKS endpoint
(IAM_ENDPOINT/.well-known/jwks). Extracts sub, owner, email claims
and sets X-User-Id, X-Org-Id, X-User-Email headers.
Also accepts gateway-propagated headers (X-User-Id) for deployments
behind Hanzo Gateway. One auth path: IAM only.
All authentication now goes through Hanzo Gateway → IAM JWKS.
The broker reads identity from gateway-propagated headers
(X-User-Id, X-Org-Id, X-User-Email, X-User-Roles) and never
validates JWTs or API keys internally.
- Remove auth.Store, auth.APIKey, API key middleware
- Remove admin.Middleware from compliance router
- Remove /auth/login and /auth/verify endpoints
- RBAC reads X-User-Roles header from gateway
- Reviewer identity from X-User-Id header
- /healthz remains unauthenticated
- Rename super_admin → superadmin
Default grant for authenticated IAM users now only covers "read" (browsing
market data). Trade permission requires explicit role assignment to prevent
pre-KYC users from placing orders on the ATS.
resolveUserID now checks X-User-Id first (set by ATS injectAuthHeaders)
before falling back to X-Gateway-User-Id and X-Hanzo-User-Id.
resolveUserAccounts short-circuits on X-Account-Id + optional
X-Account-Provider headers, skipping the ListAccounts call when the
caller already knows the sub-account.
IAM-authenticated users no longer get implicit admin access. RequirePermission
now checks X-User-Roles header for the required permission. Default IAM users
without explicit roles get read + trade only. Admin endpoints require explicit
admin role in JWT claims.
Adds hasRole() helper for comma-separated role string parsing.
resolveConid now uses POST /iserver/secdef/search for proper
multi-exchange symbol resolution. Supports TSX symbols (U.UN,
SPUT.TO), LSE (VOD.L), HKEX, etc. SMART routing auto-selects
best execution venue.
When the caller doesn't provide explicit provider→account mappings,
SmartOrder now auto-discovers the first available account for the
best provider. Fixes "no account configured" errors from ATS.