Compare commits

...
34 Commits
Author SHA1 Message Date
zeekay e78da8ddec build: fresh-origin go.sum + Dockerfile self-heal (v0.2.1)
release / build-and-push (push) Failing after 52s
v0.2.0's go.sum was regenerated from a stale module cache, so a clean container
build mismatched the force-re-tagged hanzoai/kms/sdk/go. Regenerate from fresh
origin (matches Docker's fetch), add self-heal (rm go.sum + retry on poisoning)
+ GOFLAGS=-mod=mod. New tag (not a force-re-tag of v0.2.0 — that's the anti-pattern).
2026-06-18 21:35:11 -07:00
zeekay dbe5dad2f8 build: HIP-0106 unified binary builds pure-Go static
release / build-and-push (push) Failing after 46s
- commerce v1.42.5 -> v1.42.27 (pure-Go modernc SQLite + tracked embed catalogs)
- pin luxfi/kms v1.11.6 (past force-re-tagged v1.11.0)
- exclude legacy ugorji/go (gin msgpack ambiguity)
- regenerate go.sum clean (sumdb off; force-re-tag poisoning)
- Dockerfile: GOPRIVATE + GOSUMDB=off + GOPROXY=direct + gh_token secret

Produces CGO_ENABLED=0 static /cloud (206M); all subsystems link.
Follow-up: repin hanzoai/kms off the dead v1.0.x pseudo-version to v0.159.x.
2026-06-18 21:25:23 -07:00
d77e554b7e refactor(cmd): single subsystems bundle — define the mounted set once (DRY) (#19)
cmd/cloud and cmd/hanzo each blank-imported the same 16-subsystem list, so
adding/removing a subsystem meant editing two files (repeat-yourself). Move
the list into one package, github.com/hanzoai/cloud/subsystems; both
entrypoints blank-import only that. One source of truth for what's linked into
a Hanzo binary — dispatcher and full-surface binary mount an identical set by
construction.

(Bundle is a sibling subpackage, not the root cloud package: subsystems import
cloud for Deps+Register, so a root bundle would cycle.)

Verified: go build -tags 'cloud cloud_mount' . ./subsystems ./cmd/cloud
./cmd/hanzo green (-mod=readonly); hanzo --help still lists 18 subcommands
(16 subsystems + cloud + datastore); go test ./... green.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:29:28 -07:00
e075f7a438 fix(deps): commerce v1.39.1->v1.42.5 — mount commerce into the unified binary (#18)
cloud pinned commerce v1.39.1, which predates commerce's cloud-mount
integration: cloud.Register("commerce", 100, ...) in init() behind
//go:build cloud, added at v1.42.5. So commerce silently was NOT in the fused
surface or hanzo's subcommands. v1.42.5 (latest tag) registers correctly —
`hanzo --help` now lists commerce; the unified binary composes 16 subsystems.
iam stays v1.19.4 (no cascade). go build -tags 'cloud cloud_mount' + full
go test ./... green.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:21:19 -07:00
cea54a96b0 feat(cmd): land unified hanzo binary + extract shared cloud.Serve (DRY) (#17)
Adds cmd/hanzo — one binary dispatched by subcommand: `hanzo <svc>` serves one
subsystem, `hanzo cloud` the full fused surface, `hanzo iam` the standalone
Beego IdP (iamserver.Run), `hanzo datastore` documents the ClickHouse boundary.

DRY: the cloud-server body (compose root + HIP-0106 /v1/<name>/health contract
+ graceful shutdown) is extracted from cmd/cloud's main() into cloud.Serve(enable)
— the ONE shared place. cmd/cloud now calls cloud.Serve(nil), gaining graceful
shutdown + health endpoints (strict superset of its prior body, no regression).
cmd/hanzo dispatches through the same cloud.Serve.

Beego non-collision holds: iam registers routes inside iamserver.Init(), not
package init(); one Beego v2 path; visor (Beego v1) intentionally unlinked.
iam v1.19.4 moves indirect->direct (cmd/hanzo imports iam/iamserver).

Verified: go build -tags 'cloud cloud_mount' . ./cmd/cloud ./cmd/hanzo green
(-mod=readonly), go vet clean, hanzo --help lists 17 subcommands.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:47:13 -07:00
ae8c0cea98 fix(deps): iam v1.19.1→v1.19.4 + pkg/iam v1.18.1→v1.18.4 — fix cloud-api boot panic (#16)
iam v1.19.0–v1.19.3 panic at boot: routers/router.go registered
GET /v1/iam/run-authz-command → ApiController.RunAuthzCommand, a method never
added, so Beego panics at route registration when iamserver.Init() runs (this
hit cloud-api too). v1.19.4 (cut from iam main: dangling route removed +
initAdminUser seeds via conf.AdminOrg) fixes it. Transitive zap-proto/go
v0.3.0→v1.1.0 required by pkg/iam v1.18.4. go build -tags 'cloud cloud_mount'
./cmd/cloud green; -mod=readonly verified.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:36:42 -07:00
14bda07881 fix(go.sum): re-record re-tagged hanzoai/lux module checksums (unblocks build) (#15)
A chain of re-tagged modules produced checksum SECURITY ERRORs that
block 'go build ./...':
  - github.com/hanzoai/base@v1.3.2          (h1)
  - github.com/luxfi/threshold@v1.9.4       (h1)
  - github.com/hanzoai/kms/sdk/go@v1.0.0    (h1 + go.mod)
Re-record the current proxy hashes. go build ./... -> exit 0 (cmd/cloud links).

Co-authored-by: zooqueen <dev@hanzo.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:53:19 -07:00
Abhishek KrishnaandGitHub 4de40e2396 fix(go.sum): refresh stale hash for retagged luxfi/threshold (#7)
github.com/luxfi/threshold v1.9.4 was re-pushed at the same version
tag without updating go.sum in this repo. The recorded h1: digest no
longer matches the upstream zip on proxy.golang.org, so cold clones
fail at `go mod verify` / `go build`:

  verifying github.com/luxfi/threshold@v1.9.4: checksum mismatch
      downloaded: h1:H69e9QkvygDtWkni1FkD5ztLdiTasmpJXcuVzgebdxo=
      go.sum:     h1:/TsgIzo/e/DIx++J0+9eNuS7HkpSaXbVk+HvhlUOsmE=
  SECURITY ERROR

Regenerating go.sum (single line update) restores parity with
upstream and the build succeeds.

Reproduction (pre-fix):
  git clone https://github.com/hanzoai/cloud
  cd cloud && GOPRIVATE='github.com/hanzoai/*,github.com/luxfi/*' \
    go build ./cmd/cloud
  # verifying github.com/luxfi/threshold@v1.9.4: checksum mismatch

No code changes outside go.sum.
2026-06-16 20:28:51 -07:00
Abhishek KrishnaandGitHub 7f1e0d61df ci: add release workflow to publish ghcr.io/hanzoai/cloud (#8)
The README advertises `docker run -p 8080:8080 ghcr.io/hanzoai/cloud:latest`
but there's no release pipeline in the tree to produce that image.
This workflow publishes the Docker image on git-tag push (matching
v*), on default-branch push (as :latest and :sha-<short>), and on
manual dispatch.

Includes a Buildx secret pattern for fetching private upstream
modules (hanzoai/iam, hanzoai/commerce, hanzoai/gateway, ...) using
either the default GITHUB_TOKEN or an org-level HANZO_GH_RO_TOKEN
when cross-org private reads are required.
2026-06-16 20:28:47 -07:00
Abhishek KrishnaandGitHub 18738d56b2 chore: add helm/cloud minimal chart (#9)
Reference Helm chart that renders a single Deployment + Service +
optional PVC for the unified cloud binary. Mirrors the binary's
CLI flags via .Values.hanzo (brand, domain, dataDir, iamIssuer,
enable). Pod runs as nonroot UID 65532 matching the Dockerfile.

Not a substitute for luxfi/operator + Service CRD — this chart is
for users who want raw k8s manifests without installing the
operator first.
2026-06-16 20:28:44 -07:00
Abhishek KrishnaandGitHub 942feca4d7 chore: add deploy/compose.yml for single-node VPS deployment (#6)
Reference Docker Compose manifest for the unified cloud binary, with
matching .env.example and a deploy/README.md that documents required
vs optional environment variables (the binary refuses to mount IAM
without HANZO_IAM_ISSUER, so make that explicit).

No code or config changes outside deploy/.
2026-06-16 20:28:41 -07:00
Abhishek KrishnaandGitHub ad7828553e chore(scripts): add scripts/smoke-runtime.sh — boot + probe the real binary (#11)
`cmd/cloud-smoke` (existing) exercises the in-process mount path on a
mock zip.App with two health endpoints. It does not boot the actual
`cmd/cloud` binary or hit the HTTP surface customers will use.

This script closes that gap: it builds `./cmd/cloud`, boots it under
the same default-safe `--enable` list used in deployments (omits the
`iam` subsystem until the v1.19.2 boot panic is patched), waits for
the listener to bind, then probes the five endpoints whose expected
status is fixed by the HIP-0106 contract:

  /healthz                200   process health probe
  /v1/models              200   model catalog (no auth)
  /v1/plans               200   plansvc (goja-hosted)
  /v1/pricing             200   pricingsvc (goja-hosted)
  /v1/base/collections    401   base alive, auth-gated

If any probe regresses, the script dumps the tail of the boot log and
exits non-zero — making it usable as a CI gate and as a local "does my
clone actually serve?" check.

Env knobs (`PORT`, `LISTEN`, `BIN`, `DATA_DIR`, `ENABLE`,
`KEEP_RUNNING`, `BOOT_TIMEOUT`) let it drop into different
environments without a Makefile change. The `IAM_*` env vars default
to the production hanzo.id JWKS — required by `kms` for inbound JWT
validation even with `iam` disabled — and can be overridden per
deployment.

Pairs with the Makefile in #5: `make smoke` already exists for the
mount-time path; this is the runtime counterpart and can be wired as a
sibling target (`make smoke-runtime`) in a follow-up once #5 lands.
2026-06-16 20:28:37 -07:00
Abhishek KrishnaandGitHub 08a6ef73e7 chore: add Makefile with build/test/docker targets (#5)
Minimal developer ergonomics for the unified cloud binary. Targets
wrap go build / go test / docker build for the existing Dockerfile,
plus a `make run` shortcut that matches the README quickstart
(--enable=iam,base,kms,gateway,o11y).

No code changes outside the new Makefile.
2026-06-16 20:28:34 -07:00
Abhishek KrishnaandGitHub 4e17df928d chore: add .gitignore and .dockerignore (#10)
Repo currently has neither file. Two practical consequences:

1. `docker build .` copies the entire context including `.git/`, IDE
   metadata, OS detritus (`.DS_Store`), and any local `.env` — bloats
   the build context and risks baking secrets into image layers.
2. Without a `.gitignore`, the build output binary (`/cloud` per the
   `Dockerfile` final stage), local `.env` files, and editor leftovers
   are easy to commit by accident.

Both files cover the standard Go-project surface (binary at `/cloud`,
test outputs, coverage, env files), plus IDE/OS noise. The
`.dockerignore` additionally drops docs and tests so they don't enter
the runtime image — the binary is what ships, the README lives on
GitHub.

No behavior change; the binary the Dockerfile builds is bit-identical.
What changes is build-context size and the safety margin around
accidental commits.
2026-06-16 20:28:30 -07:00
223fc137f3 fix(auth): default CLOUD_IAM_ISSUER to https://iam.hanzo.ai (was .id typo) (#14)
IAM issues JWTs with iss=https://iam.hanzo.ai, but cloud-api defaulted the
expected issuer to https://iam.hanzo.id — so EVERY hanzo.id-login JWT was
rejected with 'invalid issuer claim (iss)' and the AI gateway's JWT auth path
was dead for all users (only hk-*/sk-* keys worked). One-char .id->.ai fix.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:55:57 -07:00
8ca298873c test(cloud): drop cmd/cloud-smoke, add real orchestrator integration test (#13)
The cloud-smoke command was a throwaway harness that hand-mounted fake health
routes and avoided the real subsystem matrix (citing build issues in cmd/cloud
that are now fixed — the full binary builds). Replaced with a proper go test in
package main that exercises the actual path:

- TestRegistryAssemblesSubsystems: every subsystem main.go imports self-registers
  via init() into cloud.Registry (proves the unified binary wires the matrix).
- TestMountAllAndServeHealth: BuildDeps -> MountAll -> serve; the self-contained
  subsystems (base, authz, amqp, metrics, plans, pricing) mount in-process and
  serve /v1/<name>/health = 200 via the real zip/fiber + jsonenc stack
  (app.Fiber().Test, no listener / external services).
- TestDepGatedSubsystemsFailClosed: ai, o11y mount and return >=500 from the
  disabled-dep stub — proving the BuildDeps three-mode contract end to end.

Discovered (not fixed here — separate subsystem bug): enabling iam panics with
"'RunAuthzCommand' method doesn't exist in the controller ApiController".

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:56:21 -07:00
b90e629308 fix(go.sum): refresh moved private-tag hashes so it builds clean (#12)
hanzoai/base@v1.3.2 and hanzoai/kms/sdk/go@v1.0.0 were re-tagged upstream, so
the recorded go.sum hashes no longer matched — `go build`/`go test` failed with
checksum mismatch (SECURITY ERROR) for anyone fetching fresh. Refreshed the
private-module hashes against the current tags.

Verified: go build ./... and go test ./... pass in default -mod=readonly mode.
No local replace directives (the 9 replaces are all published version pins for
the krakend/traefik gateway stack). cmd/cloud links to a single 272M binary.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:47:05 -07:00
Antje WorringandClaude Opus 4.8 7876d86241 feat(cloud): pin metrics v0.4.0 — ZAP MsgMetricBatch receiver + per-tenant + durable
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:15:56 -07:00
Antje WorringandClaude Opus 4.8 91970431f3 feat(cloud): pin metrics v0.3.0 — per-tenant observability isolation (X-Org-Id)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:44:48 -07:00
Antje WorringandClaude Opus 4.8 50add91a84 feat(cloud): pin metrics v0.2.0 — durable native metrics+logs+traces
The unified binary now serves the full native observability stack at
/v1/{metrics,logs,traces}/* — WAL-durable (survives restart, verified), zero
prometheus, zero Grafana. This is the working replacement for the Loki/Tempo/
SigNoz vendoring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:20:43 -07:00
Antje WorringandClaude Opus 4.8 ea1a699d9e feat(cloud): mount native hanzoai/metrics v0.1.0 (ZAP-native metrics store)
The prometheus-free replacement for the Grafana/Prometheus observability
backends. Registers at order 40, serves /v1/metrics/{health,batch,write,query};
ingests luxfi/metric.MetricBatch (the ZAP MsgMetricBatch wire shape). Verified
live: write+query and batch+query roundtrips return correct series. Binary stays
at zero prometheus. (Also refreshed the stale kms/sdk/go go.sum hash from the
wave's re-tag.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:24:44 -07:00
hanzo-devandClaude Opus 4.8 10ad830e10 build(deps): re-pin plans/pricing/licensing to release tags; drop local replace
Replace the dev pseudo-versions + the local `replace github.com/hanzoai/licensing
=> ../licensing` directive with the published release tags now that the three
subsystems are merged + tagged:

  - github.com/hanzoai/plans     v1.2.0  (was pseudo @147eced7)
  - github.com/hanzoai/pricing   v1.3.0  (was pseudo @0c4b4c12)
  - github.com/hanzoai/licensing v0.1.0  (was v0.0.0 + replace => ../licensing)

licensing@v0.1.0 requires github.com/hanzoai/cloud@v0.0.0-00010101...; that
self-reference resolves to this main module, so no replace is needed. go.mod/
go.sum are tidy and `go build -mod=readonly ./cmd/cloud` produces the 303M
unified binary (boots with --enable=plans,pricing,licensing; all health 200).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:58:14 -07:00
hanzo-devandClaude Opus 4.8 58bb1843a4 Merge feat/plans-pricing-goja-clean into feat/mount-licensing
Combine the licensing Mount (PR #3) with the plans+pricing goja mounts
(PR #4) onto one branch for the unified-binary re-pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:54:22 -07:00
hanzo-devandClaude Opus 4.8 bb2600bd9b feat(plans+pricing): mount @hanzo/plans + @hanzo/pricing via base+goja
Mount the Node @hanzo/plans (data) and @hanzo/pricing (Express) services
INTO the unified cloud binary under /v1/plans/* and /v1/pricing/*, running
their JS inside the dop251/goja engine (the same engine base/plugins/gojavm
uses) — per HIP-0106. No service rewrite; Ship-of-Theseus to pure Go later.

New packages (the glue — clean module boundaries, no service source copied):
  - clients/gojahost: a reusable goja VM-pool host. Compiles a service repo's
    goja/bundle.js once, pre-warms a runtime pool (mirrors gojavm's pool +
    compile-once + per-runtime ensureLoaded discipline), injects the catalog
    JSON as globals, and dispatches handle({route,params,tenant}) -> {status,
    body} with ctx-cancel interrupt. Eager-loads one runtime so a bad bundle
    fails at Mount, not first request.
  - clients/plansvc: Mount(app, deps) for /v1/plans/*. Loads the @hanzo/plans
    bundle + embedded catalog, registers zip routes (subscriptions, cloud,
    blockchain, dns, gpu, regions, storage, tools, policy, schema, vocab,
    resolve/:id, entitlements/:id), threads X-Org-Id as the tenant for
    per-reseller (tenant_id,id) catalog scoping. The entitlements.mjs
    transforms (fromLegacy/toLicenseFeatures/resolvePlan) run in goja.
  - clients/pricingsvc: Mount(app, deps) for /v1/pricing/* + /v1/models.
    Express does NOT run in goja, so the Express transport is dropped; the
    server.mjs read handlers run in goja via the bundle. The sync.mjs markup
    (toMTok/processOpenRouterModel/...) also runs in goja via applyMarkup();
    the admin-gated POST /v1/pricing/sync does the live OpenRouter fetch in Go
    (net/http) and feeds raw JSON into the goja markup. _internal (provider
    costs/routing) is stripped from public responses.

Wiring: cmd/cloud/main.go blank-imports both wrappers; each init() calls
cloud.Register (plans order 111, pricing 112, after iam/commerce/licensing).
go.mod references hanzoai/plans + hanzoai/pricing as their own private Go
modules (the JS + data live there, embedded; nothing copied into cloud).

Tests: clients/{gojahost,plansvc,pricingsvc}/*_test.go exercise the real
embedded bundles (vocab, resolve+license_features, 404s, _internal strip,
exact markup math). Verified end-to-end: binary boots with --enable=plans,
pricing; all routes serve real data through goja over HTTP; X-Org-Id tenant
scoping confirmed (reseller override-wins, isolation holds).

Also corrects a stale go.sum entry for github.com/hanzoai/kms/sdk/go@v1.0.0
(the module was retagged; the recorded hash no longer matched the origin,
blocking any build that pulls base/iam/commerce -> kms). Updated to the
current origin hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 18:42:54 -07:00
hanzo-devandClaude Opus 4.8 5c6beb850b feat(cloud): mount licensing subsystem + commerce entitlement-copy
Wire the private hanzoai/licensing Go subsystem into the unified cloud
binary per HIP-0106, following the iam/commerce/ai Mount(app, deps)
pattern. Clean module boundary: licensing is imported as its own private
module (its mount.go self-registers via init(); cmd/cloud blank-imports
it). No subsystem source is copied into cloud — the signer/fingerprint
secret logic stays in the licensing module.

- cmd/cloud: blank-import github.com/hanzoai/licensing (order 110, after
  iam=50 and commerce=100, since the /v1/licensing/issue flow depends on
  both identity and entitlements).
- go.mod: require licensing + local replace (co-developed private module;
  production resolves via tag/pseudo-version, drop the replace).
- types.CommerceClient: add CheckEntitlement(ctx, orgID, productID) plus
  the LicenseEntitlement transport type. This is the entitlement flow that
  gates issuance: commerce answers "does this tenant own the licensed
  product?" and returns the plan's FLAT license-features per the
  @hanzo/plans toLicenseFeatures vocab contract; the licensing mount copies
  them verbatim into the signed token's `features` so the engine enforces
  exactly the plan that was bought.
- clients: implement CheckEntitlement on the disabled (fail-closed) and
  ZAP-RPC commerce stubs; in-process pass-through already satisfies it.

Tenant-scoped via orgID (X-Org-Id). Real KMS stays a licensing follow-up
(scaffold TODO); the Mount + entitlement-copy are the deliverable here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 18:23:19 -07:00
Antje WorringandClaude Opus 4.8 1e1e017ee4 fix(cloud): pin gateway v2.9.7 (clean, no local replaces)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:30:42 -07:00
Antje WorringandClaude Opus 4.8 991ec0e788 fix(cloud): correct gateway pin to v2.9.6+incompatible (prev go.mod was broken)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:46:14 -07:00
Antje WorringandClaude Opus 4.8 8b59797b59 fix(cloud): pin gateway v2.9.6 — ZERO prometheus in the unified binary
gateway dropped the legacy opencensus SaaS exporters (stackdriver was the last
prometheus source). Combined with o11y v1.3.7 + alertmanager/krakend-otel forks +
base v1.3.2 + kms Corona, the binary now links zero real prometheus packages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:45:19 -07:00
Antje WorringandClaude Opus 4.8 e67bd8aa6d fix(cloud): pin base v1.3.2 — network metrics on luxfi/metric (prometheus 6->1)
Real prometheus in the unified binary is now a single leaf package
(prometheus/prometheus/model/value via a gateway dep). Down from 16+.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:44:05 -07:00
Antje WorringandClaude Opus 4.8 ee879c028b fix(cloud): prometheus 16->6 pkgs — krakend-otel fork + gateway v2.9.5
replace krakend-otel => hanzoai/krakend-otel v0.13.1 (prom-free fork); pin
gateway v2.9.5 (opencensus prometheus exporter removed, counters -> luxfi/metric).
With the alertmanager fork + o11y v1.3.7 + iam v1.18.1, the only prometheus left
is the hanzoai/common+alertmanager fork shim core (6 pkgs) — needs those forks to
shed prometheus/common internally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:41:05 -07:00
Antje WorringandClaude Opus 4.8 e0680a7030 fix(cloud): pin iam/pkg/iam v1.18.1 — kill duplicate-beego graceful flag panic
pkg/iam v1.18.0 imported upstream github.com/beego/beego/v2 while the rest of the
binary uses the hanzoai/beego prom-free fork; both register a global 'graceful'
flag in init() -> panic at startup. v1.18.1 (already fixed on iam main, just
untagged) uses the fork only. The unified binary now boots and shows the
white-label -brand/-domain/-enable tenancy flags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:11:47 -07:00
Antje WorringandClaude Opus 4.8 952a5112b3 fix(cloud): green the unified binary — o11y v1.3.7 + alertmanager fork replace
o11y's prometheus->hanzoai/alertmanager replace must be carried by the main
module (cloud), since a dependency's replace is ignored by consumers. With
o11y v1.3.7 (no-prometheus) + kms v0.159.1 (Corona), the full HIP-0106 binary
(11 subsystems on zip+ZAP, /v1 routing, luxfi/log+metric) compiles end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:06:39 -07:00
Antje WorringandClaude Opus 4.8 d60e476ea8 fix(deps): pin hanzoai/kms v0.159.1 (Corona signing fix)
Clears the kms SignWithRingtail blocker. cloud's remaining build failure is
hanzoai/o11y v1.3.6 (incomplete prometheus/common -> hanzoai/common fork).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:49:18 -07:00
Antje WorringandClaude Opus 4.8 66574461da fix(deps): drop 13 cross-repo local replaces; pin siblings to real published versions
Build still blocked separately on kms SignWithRingtail (luxfi/kms API gap) and
o11y type-mixing — tracked upstream; this lands the no-local-replace requirement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:11:00 -07:00
37 changed files with 2833 additions and 1185 deletions
+41
View File
@@ -0,0 +1,41 @@
# VCS
.git/
.gitignore
.gitattributes
# CI / repo metadata not needed inside the build
.github/
# Docs (image runs the binary; readers visit GitHub)
*.md
LICENSE
SECURITY.md
# Already-built binary at repo root (matches .gitignore)
/cloud
# Environment files (never bake secrets into images)
.env
.env.*
# IDE / editor
.vscode/
.idea/
*.swp
*.swo
# OS metadata
.DS_Store
Thumbs.db
# Tests stay out of the runtime image
*_test.go
# Local build outputs
/dist/
/build/
/bin/
# The Dockerfile itself doesn't need to be in the context it builds
Dockerfile
.dockerignore
+87
View File
@@ -0,0 +1,87 @@
name: release
# Builds and pushes ghcr.io/hanzoai/cloud:<tag> when a git tag matching v*
# is pushed. Also publishes :sha-<short> and :latest for the default branch.
#
# Notes for maintainers:
# - The cloud binary depends on private upstream modules
# (hanzoai/iam, hanzoai/commerce, hanzoai/gateway, etc). The default
# GITHUB_TOKEN issued by Actions does NOT have read access across
# private repos in other orgs. If that's the case here, configure an
# org-level secret HANZO_GH_RO_TOKEN with read:packages + repo scope
# covering hanzoai/* and luxfi/*, and replace the GOPRIVATE step's
# token reference.
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
packages: write
id-token: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Log in to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Configure private module access for Go (build args)
id: gomod-token
run: |
# Prefer an org-level token if configured (covers cross-org
# private deps); fall back to the default workflow token.
if [ -n "${{ secrets.HANZO_GH_RO_TOKEN }}" ]; then
echo "token=${{ secrets.HANZO_GH_RO_TOKEN }}" >> "$GITHUB_OUTPUT"
else
echo "token=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_OUTPUT"
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
secrets: |
"gh_token=${{ steps.gomod-token.outputs.token }}"
# If the Dockerfile needs the token at build-time for `go mod
# download` of private modules, set the BuildKit secret above
# and reference it in the Dockerfile with:
# RUN --mount=type=secret,id=gh_token \
# git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/" && \
# GOPRIVATE='github.com/hanzoai/*,github.com/luxfi/*' go mod download
+32
View File
@@ -0,0 +1,32 @@
# Built binary (matches Dockerfile output path)
/cloud
# Local build directories
/dist/
/build/
/bin/
# Go test + coverage artifacts
*.test
*.out
coverage.txt
coverage.html
# Environment files
.env
.env.*
!.env.example
# IDE / editor
.vscode/
.idea/
*.swp
*.swo
*~
# OS metadata
.DS_Store
Thumbs.db
# Logs
*.log
+20 -1
View File
@@ -1,7 +1,26 @@
FROM golang:1.26-alpine AS build
RUN apk add --no-cache ca-certificates tzdata
RUN apk add --no-cache ca-certificates tzdata git
RUN addgroup -g 65532 -S nonroot && adduser -u 65532 -S nonroot -G nonroot
WORKDIR /src
# Private cross-org subsystem modules (hanzoai/*, luxfi/*, zap-proto/*) are
# fetched via authenticated git. GOSUMDB=off + GOPROXY=direct tolerate
# force-re-tagged luxfi/hanzoai modules (committed go.sum is source of truth);
# gh_token is the shared docker-build.yml BuildKit secret (no-op when absent).
ENV GOPRIVATE=github.com/hanzoai/*,github.com/luxfi/*,github.com/zap-proto/* \
GOSUMDB=off \
GOPROXY=direct \
GOFLAGS=-mod=mod
COPY go.mod go.sum ./
# go mod download, self-healing past upstream force-re-tag poisoning: if a
# luxfi/hanzoai module's tag was force-moved/deleted after go.sum was recorded,
# the committed sum mismatches the fresh origin fetch. With GOSUMDB=off the
# regenerated sum is trustworthy for our own private modules. Root-cause fix is
# stopping force-re-tags at the source; this keeps the image buildable meanwhile.
RUN --mount=type=secret,id=gh_token \
if [ -s /run/secrets/gh_token ]; then \
git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/"; \
fi && \
(go mod download || (echo ">> go.sum poisoned by upstream force-re-tag; regenerating from origin" && rm -f go.sum && go mod download))
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /cloud ./cmd/cloud
+43
View File
@@ -0,0 +1,43 @@
# hanzoai/cloud — developer ergonomics for the unified Hanzo Cloud binary (HIP-0106).
# Targets are intentionally minimal; deploy artifacts (compose, helm) live in deploy/ and helm/.
GO ?= go
BIN ?= cloud
PKG ?= ./cmd/cloud
DOCKER_IMAGE ?= ghcr.io/hanzoai/cloud
DOCKER_TAG ?= dev
LDFLAGS ?= -s -w
.PHONY: help build run smoke test vet tidy docker docker-push clean
help: ## Show this help.
@awk 'BEGIN{FS=":.*##";printf "\nUsage: make <target>\n\nTargets:\n"} /^[a-zA-Z_-]+:.*##/{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
build: ## Build the unified cloud binary into ./bin/cloud.
@mkdir -p bin
$(GO) build -ldflags="$(LDFLAGS)" -o bin/$(BIN) $(PKG)
run: build ## Run with iam,base,kms,gateway,o11y enabled (matches README quickstart).
./bin/$(BIN) --enable=iam,base,kms,gateway,o11y --brand=hanzo --domain=api.hanzo.ai
smoke: ## Build and run cmd/cloud-smoke (mount-time integration check).
$(GO) run ./cmd/cloud-smoke
test: ## Run unit + integration tests.
$(GO) test ./...
vet: ## go vet across the module.
$(GO) vet ./...
tidy: ## go mod tidy + verify go.sum.
$(GO) mod tidy
$(GO) mod verify
docker: ## Build the Docker image (uses repo Dockerfile, scratch final stage).
docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
docker-push: docker ## Push the Docker image to ghcr.io. Requires docker login.
docker push $(DOCKER_IMAGE):$(DOCKER_TAG)
clean: ## Remove built artifacts.
rm -rf bin
+3
View File
@@ -63,6 +63,9 @@ type disabledCommerce struct{}
func (disabledCommerce) GetTenantConfig(_ context.Context, _ string) (*types.TenantConfig, error) {
return nil, &disabledErr{"commerce"}
}
func (disabledCommerce) CheckEntitlement(_ context.Context, _, _ string) (*types.LicenseEntitlement, error) {
return nil, &disabledErr{"commerce"}
}
type disabledAI struct{}
+366
View File
@@ -0,0 +1,366 @@
// Package gojahost runs a Hanzo Node service's goja bundle (a self-contained,
// ESM-free JS file exposing globalThis.handle(req)) inside the unified cloud
// binary, per HIP-0106.
//
// It is the SHARED glue used by clients/plansvc and clients/pricingsvc to host
// @hanzo/plans and @hanzo/pricing in-process via dop251/goja — the same engine
// base/plugins/gojavm uses. We do not import base's gojavm Runtime directly
// because that loader is manifest-driven (extension.json + a single exported
// `fn` over JSON-over-the-wire payloads); our services instead inject a catalog
// of JSON globals at VM init and call a richer handle({route,params,...}) entry.
// The VM-pool + compile-once + per-runtime ensureLoaded discipline here mirrors
// gojavm/runtime.go exactly so behavior and the pool semantics are identical.
//
// Module boundary: the JS bundle + catalog data live in the service repos
// (hanzoai/plans, hanzoai/pricing) and are passed in by the caller. This
// package carries zero service logic — only the engine plumbing.
package gojahost
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"sync"
"github.com/dop251/goja"
)
// defaultPoolSize mirrors gojavm's default. Override via CLOUD_GOJAHOST_POOL_SIZE.
const defaultPoolSize = 8
// Request is the dispatch envelope handed to globalThis.handle in JS.
type Request struct {
Route string `json:"route"`
Params map[string]string `json:"params,omitempty"`
Query map[string]string `json:"query,omitempty"`
Tenant string `json:"tenant,omitempty"`
}
// Response is what globalThis.handle returns: an HTTP status + an opaque body
// that the host serializes straight to JSON.
type Response struct {
Status int `json:"status"`
Body json.RawMessage `json:"body"`
}
// Host is a compiled service bundle plus a pool of goja runtimes that have had
// the bundle + the injected globals evaluated. Safe for concurrent use.
type Host struct {
name string
program *goja.Program
globals map[string]any
pool []*slot
factory func() *goja.Runtime
mu sync.Mutex
closed bool
}
type slot struct {
mu sync.Mutex
busy bool
vm *goja.Runtime
loaded bool
hasFunc bool
}
// Config configures a Host.
type Config struct {
// Name identifies the service for error messages ("plans", "pricing").
Name string
// Bundle is the goja bundle source (goja/bundle.js from the service repo).
Bundle []byte
// Globals are injected onto each runtime before the bundle runs, e.g.
// {"__PLANS_DATA__": <catalog>}. Values are converted via goja.ToValue.
// Pointers to the same Go value are shared read-only across runtimes; the
// bundles never mutate injected globals.
Globals map[string]any
}
// New compiles the bundle and pre-warms the runtime pool. The bundle is
// compiled once (goja.Program is safe to share across runtimes); each pool
// runtime evaluates it lazily on first use.
func New(cfg Config) (*Host, error) {
if cfg.Name == "" {
return nil, errors.New("gojahost: Config.Name required")
}
if len(cfg.Bundle) == 0 {
return nil, fmt.Errorf("gojahost[%s]: empty bundle", cfg.Name)
}
prog, err := goja.Compile(cfg.Name+"/bundle.js", string(cfg.Bundle), true)
if err != nil {
return nil, fmt.Errorf("gojahost[%s]: compile: %w", cfg.Name, err)
}
size := defaultPoolSize
if v := os.Getenv("CLOUD_GOJAHOST_POOL_SIZE"); v != "" {
if n, e := strconv.Atoi(v); e == nil {
size = n
}
}
if size < 1 {
size = 1
}
h := &Host{
name: cfg.Name,
program: prog,
globals: cfg.Globals,
factory: func() *goja.Runtime { return goja.New() },
pool: make([]*slot, size),
}
for i := range h.pool {
h.pool[i] = &slot{vm: h.factory()}
}
// Eagerly load + validate one runtime so misconfiguration (bad bundle,
// missing handle export) fails at Mount, not at first request.
if err := h.withSlot(func(s *slot) error {
if err := h.ensure(s); err != nil {
return err
}
if !s.hasFunc {
return fmt.Errorf("gojahost[%s]: bundle does not define globalThis.handle", cfg.Name)
}
return nil
}); err != nil {
return nil, err
}
return h, nil
}
// ensure evaluates the bundle on a runtime exactly once (installing the
// injected globals first), then records whether globalThis.handle exists.
func (h *Host) ensure(s *slot) error {
if s.loaded {
return nil
}
for k, v := range h.globals {
if err := s.vm.Set(k, v); err != nil {
return fmt.Errorf("gojahost[%s]: set global %s: %w", h.name, k, err)
}
}
// Minimal node-ish shims the bundles may touch. The bundles are written
// to avoid console, but defensively wire a no-op console so a stray
// console.* never throws ReferenceError.
installConsole(s.vm)
if _, err := s.vm.RunProgram(h.program); err != nil {
return fmt.Errorf("gojahost[%s]: run bundle: %w", h.name, err)
}
_, s.hasFunc = goja.AssertFunction(s.vm.Get("handle"))
s.loaded = true
return nil
}
// Dispatch calls globalThis.handle(req) on a pooled runtime and returns the
// JS-side {status, body}. ctx cancellation interrupts the call.
func (h *Host) Dispatch(ctx context.Context, req Request) (*Response, error) {
h.mu.Lock()
if h.closed {
h.mu.Unlock()
return nil, fmt.Errorf("gojahost[%s]: closed", h.name)
}
h.mu.Unlock()
if err := ctx.Err(); err != nil {
return nil, err
}
var resp *Response
err := h.withSlot(func(s *slot) error {
if err := h.ensure(s); err != nil {
return err
}
fn, ok := goja.AssertFunction(s.vm.Get("handle"))
if !ok {
return fmt.Errorf("gojahost[%s]: globalThis.handle missing", h.name)
}
// Watchdog: interrupt the VM if ctx cancels mid-call.
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-ctx.Done():
s.vm.Interrupt(ctx.Err())
case <-done:
}
}()
arg := s.vm.ToValue(map[string]any{
"route": req.Route,
"params": toAnyMap(req.Params),
"query": toAnyMap(req.Query),
"tenant": req.Tenant,
})
out, callErr := fn(goja.Undefined(), arg)
if callErr != nil {
var iex *goja.InterruptedError
if errors.As(callErr, &iex) {
if ce := ctx.Err(); ce != nil {
return ce
}
}
return fmt.Errorf("gojahost[%s]: handle(%s): %w", h.name, req.Route, callErr)
}
// The JS returns {status:number, body:any}. Pull them out and
// re-marshal body to canonical JSON bytes via the export view.
exported := out.Export()
m, ok := exported.(map[string]any)
if !ok {
return fmt.Errorf("gojahost[%s]: handle(%s) returned %T, want object", h.name, req.Route, exported)
}
status := 200
if sv, ok := m["status"]; ok {
if f, ok := sv.(int64); ok {
status = int(f)
} else if f, ok := sv.(float64); ok {
status = int(f)
}
}
bodyBytes, mErr := json.Marshal(m["body"])
if mErr != nil {
return fmt.Errorf("gojahost[%s]: marshal body: %w", h.name, mErr)
}
resp = &Response{Status: status, Body: bodyBytes}
return nil
})
if err != nil {
return nil, err
}
return resp, nil
}
// Eval runs an arbitrary JS expression against a pooled runtime (bundle
// already loaded) and returns the exported Go value. Used by callers that
// want to invoke a non-route helper the bundle exposes (e.g. applyMarkup).
func (h *Host) Eval(ctx context.Context, fnName string, jsonArg []byte) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
var out []byte
err := h.withSlot(func(s *slot) error {
if err := h.ensure(s); err != nil {
return err
}
fn, ok := goja.AssertFunction(s.vm.Get(fnName))
if !ok {
return fmt.Errorf("gojahost[%s]: globalThis.%s is not a function", h.name, fnName)
}
var arg any
if len(jsonArg) > 0 {
if err := json.Unmarshal(jsonArg, &arg); err != nil {
return fmt.Errorf("gojahost[%s]: %s arg not JSON: %w", h.name, fnName, err)
}
}
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-ctx.Done():
s.vm.Interrupt(ctx.Err())
case <-done:
}
}()
res, callErr := fn(goja.Undefined(), s.vm.ToValue(arg))
if callErr != nil {
var iex *goja.InterruptedError
if errors.As(callErr, &iex) {
if ce := ctx.Err(); ce != nil {
return ce
}
}
return fmt.Errorf("gojahost[%s]: %s: %w", h.name, fnName, callErr)
}
b, mErr := json.Marshal(res.Export())
if mErr != nil {
return fmt.Errorf("gojahost[%s]: marshal %s result: %w", h.name, fnName, mErr)
}
out = b
return nil
})
return out, err
}
// SetGlobal updates an injected global and forces every pooled runtime to
// re-evaluate the bundle on next use (so the new value takes effect). Used by
// the pricing sync path to swap in freshly-synced data.
func (h *Host) SetGlobal(key string, value any) {
h.mu.Lock()
defer h.mu.Unlock()
if h.globals == nil {
h.globals = map[string]any{}
}
h.globals[key] = value
for _, s := range h.pool {
s.mu.Lock()
s.loaded = false // re-run bundle with new globals on next ensure
s.mu.Unlock()
}
}
// Close drops the pool.
func (h *Host) Close() error {
h.mu.Lock()
h.closed = true
h.pool = nil
h.mu.Unlock()
return nil
}
// withSlot borrows a free pool slot; if all are busy it spins up a one-off
// runtime (matching gojavm's saturation fallback).
func (h *Host) withSlot(call func(*slot) error) error {
h.mu.Lock()
pool := h.pool
h.mu.Unlock()
for _, s := range pool {
s.mu.Lock()
if s.busy {
s.mu.Unlock()
continue
}
s.busy = true
s.mu.Unlock()
err := call(s)
s.mu.Lock()
s.busy = false
s.mu.Unlock()
return err
}
// Saturated: ephemeral runtime, fully loaded fresh.
tmp := &slot{vm: h.factory()}
return call(tmp)
}
func toAnyMap(m map[string]string) map[string]any {
if m == nil {
return map[string]any{}
}
out := make(map[string]any, len(m))
for k, v := range m {
out[k] = v
}
return out
}
// installConsole wires a no-op console.{log,info,warn,error,debug} so guest
// code that logs does not throw. goja has no console by default.
func installConsole(vm *goja.Runtime) {
if !goja.IsUndefined(vm.Get("console")) {
return
}
noop := func(goja.FunctionCall) goja.Value { return goja.Undefined() }
console := vm.NewObject()
for _, m := range []string{"log", "info", "warn", "error", "debug", "trace"} {
_ = console.Set(m, noop)
}
_ = vm.Set("console", console)
}
+132
View File
@@ -0,0 +1,132 @@
package gojahost
import (
"context"
"encoding/json"
"testing"
"time"
)
const echoBundle = `
(function(){
globalThis.handle = function(req){
if (req.route === 'boom') throw new Error('kaboom');
if (req.route === 'notfound') return { status: 404, body: { error: 'nope' } };
return { status: 200, body: { route: req.route, tenant: req.tenant, params: req.params, data: globalThis.__X__ } };
};
globalThis.dbl = function(n){ return n * 2; };
})();
`
func newTestHost(t *testing.T) *Host {
t.Helper()
h, err := New(Config{
Name: "test",
Bundle: []byte(echoBundle),
Globals: map[string]any{"__X__": map[string]any{"k": "v"}},
})
if err != nil {
t.Fatalf("New: %v", err)
}
return h
}
func TestDispatch_OK(t *testing.T) {
h := newTestHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), Request{Route: "ping", Tenant: "acme", Params: map[string]string{"id": "7"}})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
if resp.Status != 200 {
t.Fatalf("status = %d, want 200", resp.Status)
}
var body struct {
Route string `json:"route"`
Tenant string `json:"tenant"`
Params map[string]string `json:"params"`
Data map[string]string `json:"data"`
}
if err := json.Unmarshal(resp.Body, &body); err != nil {
t.Fatalf("unmarshal %q: %v", resp.Body, err)
}
if body.Route != "ping" || body.Tenant != "acme" || body.Params["id"] != "7" || body.Data["k"] != "v" {
t.Fatalf("unexpected body: %+v", body)
}
}
func TestDispatch_Status404(t *testing.T) {
h := newTestHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), Request{Route: "notfound"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
if resp.Status != 404 {
t.Fatalf("status = %d, want 404", resp.Status)
}
}
func TestDispatch_JSThrowIsError(t *testing.T) {
h := newTestHost(t)
defer h.Close()
if _, err := h.Dispatch(context.Background(), Request{Route: "boom"}); err == nil {
t.Fatal("expected error from thrown JS exception")
}
}
func TestNew_RejectsBundleWithoutHandle(t *testing.T) {
_, err := New(Config{Name: "x", Bundle: []byte(`globalThis.notHandle = 1;`)})
if err == nil {
t.Fatal("expected error when bundle has no globalThis.handle")
}
}
func TestEval_Helper(t *testing.T) {
h := newTestHost(t)
defer h.Close()
out, err := h.Eval(context.Background(), "dbl", []byte(`21`))
if err != nil {
t.Fatalf("Eval: %v", err)
}
if string(out) != "42" {
t.Fatalf("Eval dbl(21) = %s, want 42", out)
}
}
func TestSetGlobal_TakesEffect(t *testing.T) {
h := newTestHost(t)
defer h.Close()
h.SetGlobal("__X__", map[string]any{"k": "updated"})
resp, err := h.Dispatch(context.Background(), Request{Route: "ping"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
var body struct {
Data map[string]string `json:"data"`
}
_ = json.Unmarshal(resp.Body, &body)
if body.Data["k"] != "updated" {
t.Fatalf("global not updated: %+v", body.Data)
}
}
func TestDispatch_ContextCancel(t *testing.T) {
h, err := New(Config{
Name: "loop",
Bundle: []byte(`globalThis.handle = function(){ var x=0; while(true){ x=(x+1)|0; Math.sin(x); } };`),
})
if err != nil {
t.Fatalf("New: %v", err)
}
defer h.Close()
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
start := time.Now()
if _, err := h.Dispatch(ctx, Request{Route: "x"}); err == nil {
t.Fatal("expected ctx error from infinite loop")
}
if time.Since(start) > 2*time.Second {
t.Fatalf("interrupt too slow: %v", time.Since(start))
}
}
+168
View File
@@ -0,0 +1,168 @@
// Package plansvc mounts the @hanzo/plans catalog into the unified cloud
// binary under /v1/plans/*, per HIP-0106.
//
// STRATEGY: wrap, don't rewrite. @hanzo/plans is a Node data package (JSON
// catalog + entitlements.mjs transforms). We do NOT reimplement the entitlement
// vocabulary in Go and we do NOT copy the catalog into cloud. Instead:
//
// - github.com/hanzoai/plans (the service repo's Go embed module) ships
// goja/bundle.js — the ESM-free port of entitlements.mjs + the /v1/plans
// route table — plus the embedded *.json catalog (plans.Data()).
// - This wrapper loads that bundle into a goja runtime (clients/gojahost),
// injects the catalog as globalThis.__PLANS_DATA__, and registers thin zip
// handlers that call globalThis.handle({route, params, tenant}). The
// entitlement transforms (fromLegacy/toLicenseFeatures/resolvePlan) run in
// goja — real JS, not a Go reimplementation.
//
// The plans data is read-only public-catalog content; there are no secrets
// here. The licensing SIGNER/fingerprint that consumes toLicenseFeatures stays
// in hanzoai/licensing. This wrapper is pure glue.
//
// IAM gating + X-Org-Id tenant scope: every /v1/plans route reads the
// gateway-minted identity off the zip.Ctx (c.Org()) and threads it into the
// bundle as the tenant, so a reseller org (tenant_id != "hanzo") sees its own
// catalog overrides. The plan catalog is readable by any authenticated caller;
// no admin scope is required for reads.
package plansvc
import (
"context"
"fmt"
"net/http"
"github.com/hanzoai/cloud"
"github.com/hanzoai/cloud/clients/gojahost"
hplans "github.com/hanzoai/plans"
"github.com/hanzoai/zip"
)
// host is the process-global goja host for the plans bundle. nil before Mount.
var host *gojahost.Host
// Mount registers the /v1/plans/* surface on app per HIP-0106.
func Mount(app *zip.App, deps cloud.Deps) error {
if app == nil {
return fmt.Errorf("plansvc.Mount: nil zip.App")
}
logger := deps.Logger
if logger == nil {
return fmt.Errorf("plansvc.Mount: nil deps.Logger")
}
logger = logger.New("subsystem", "plans")
bundle, err := hplans.Bundle()
if err != nil {
return fmt.Errorf("plansvc.Mount: load bundle: %w", err)
}
data, err := hplans.Data()
if err != nil {
return fmt.Errorf("plansvc.Mount: load catalog: %w", err)
}
h, err := gojahost.New(gojahost.Config{
Name: "plans",
Bundle: bundle,
Globals: map[string]any{"__PLANS_DATA__": data},
})
if err != nil {
return fmt.Errorf("plansvc.Mount: goja host: %w", err)
}
host = h
// Native health endpoint — always answers, no JS, no auth.
app.Get("/v1/plans/health", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]any{"status": "ok", "service": "plans"})
})
// Fixed-route handlers. Each maps a path to a bundle route name.
// gateway-minted identity (c.Org()) becomes the tenant for catalog scoping.
type binding struct{ path, route string }
fixed := []binding{
{"/v1/plans", "plans"},
{"/v1/plans/subscriptions", "subscriptions"},
{"/v1/plans/cloud", "cloud"},
{"/v1/plans/blockchain", "blockchain"},
{"/v1/plans/dns", "dns"},
{"/v1/plans/gpu", "gpu"},
{"/v1/plans/regions", "regions"},
{"/v1/plans/storage", "storage"},
{"/v1/plans/tools", "tools"},
{"/v1/plans/policy", "policy"},
{"/v1/plans/schema", "schema"},
{"/v1/plans/vocab", "vocab"},
}
for _, b := range fixed {
route := b.route
app.Get(b.path, func(c *zip.Ctx) error {
return dispatch(c, route, nil)
})
}
// Parameterized: resolve + entitlements take a plan id.
app.Get("/v1/plans/resolve/:id", func(c *zip.Ctx) error {
return dispatch(c, "resolve", map[string]string{"id": c.Param("id")})
})
app.Get("/v1/plans/entitlements/:id", func(c *zip.Ctx) error {
return dispatch(c, "entitlements", map[string]string{"id": c.Param("id")})
})
logger.Info("plans mounted",
"prefix", "/v1/plans",
"routes", len(fixed)+2,
"brand", deps.Brand,
)
return nil
}
// dispatch runs one bundle route on the shared goja host and writes the
// {status, body} back as JSON. The tenant is the gateway-minted org (X-Org-Id
// per HIP-0026) so reseller catalogs resolve correctly.
func dispatch(c *zip.Ctx, route string, params map[string]string) error {
if host == nil {
return c.JSON(http.StatusServiceUnavailable, map[string]any{
"error": "plans not initialised",
})
}
tenant := c.Org()
if tenant == "" {
tenant = "hanzo"
}
resp, err := host.Dispatch(c.Context(), gojahost.Request{
Route: route,
Params: params,
Tenant: tenant,
})
if err != nil {
c.Log().Error("plans dispatch failed", "route", route, "err", err)
return c.JSON(http.StatusInternalServerError, map[string]any{
"error": "plans dispatch failed",
})
}
return c.Bytes(resp.Status, withContentType(c, resp.Body))
}
// withContentType sets application/json and returns the bytes unchanged.
func withContentType(c *zip.Ctx, b []byte) []byte {
c.SetHeader("Content-Type", "application/json")
return b
}
func init() {
cloud.Register("plans", 111, func(app any, deps cloud.Deps) error {
a, ok := app.(*zip.App)
if !ok {
return fmt.Errorf("plansvc.Mount: app is %T, want *zip.App", app)
}
return Mount(a, deps)
})
}
// Shutdown drops the goja host. Idempotent.
func Shutdown(context.Context) error {
if host == nil {
return nil
}
err := host.Close()
host = nil
return err
}
+116
View File
@@ -0,0 +1,116 @@
package plansvc
import (
"context"
"encoding/json"
"testing"
"github.com/hanzoai/cloud/clients/gojahost"
hplans "github.com/hanzoai/plans"
)
// newHost loads the REAL @hanzo/plans goja bundle + embedded catalog, so this
// test exercises the actual entitlements.mjs port running in goja.
func newHost(t *testing.T) *gojahost.Host {
t.Helper()
bundle, err := hplans.Bundle()
if err != nil {
t.Fatalf("Bundle: %v", err)
}
data, err := hplans.Data()
if err != nil {
t.Fatalf("Data: %v", err)
}
h, err := gojahost.New(gojahost.Config{
Name: "plans",
Bundle: bundle,
Globals: map[string]any{"__PLANS_DATA__": data},
})
if err != nil {
t.Fatalf("gojahost.New: %v", err)
}
return h
}
func TestPlans_Vocab(t *testing.T) {
h := newHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "vocab", Tenant: "hanzo"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
var body struct {
Namespaces []string `json:"namespaces"`
Keys map[string]any `json:"keys"`
}
if err := json.Unmarshal(resp.Body, &body); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(body.Namespaces) != 9 {
t.Fatalf("namespaces = %d, want 9", len(body.Namespaces))
}
if len(body.Keys) < 40 {
t.Fatalf("entitlement keys = %d, want >=40", len(body.Keys))
}
}
func TestPlans_ResolveProducesLicenseFeatures(t *testing.T) {
h := newHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "resolve", Tenant: "hanzo", Params: map[string]string{"id": "pro"}})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
if resp.Status != 200 {
t.Fatalf("status = %d, want 200 (body=%s)", resp.Status, resp.Body)
}
var body struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
Entitlements map[string]any `json:"entitlements"`
LicenseFeatures []string `json:"license_features"`
}
if err := json.Unmarshal(resp.Body, &body); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if body.ID != "pro" || body.TenantID != "hanzo" {
t.Fatalf("id/tenant = %q/%q", body.ID, body.TenantID)
}
if len(body.Entitlements) == 0 {
t.Fatal("expected non-empty entitlements for pro")
}
if body.LicenseFeatures == nil {
t.Fatal("expected license_features array (the engine gate input)")
}
}
func TestPlans_Resolve404(t *testing.T) {
h := newHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "resolve", Tenant: "hanzo", Params: map[string]string{"id": "does-not-exist"}})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
if resp.Status != 404 {
t.Fatalf("status = %d, want 404", resp.Status)
}
}
func TestPlans_TenantScopingFallsBackToHanzo(t *testing.T) {
h := newHost(t)
defer h.Close()
// A reseller with no overrides sees the hanzo default catalog.
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "subscriptions", Tenant: "acme-reseller"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
var body struct {
Plans []map[string]any `json:"plans"`
}
if err := json.Unmarshal(resp.Body, &body); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(body.Plans) == 0 {
t.Fatal("reseller should fall back to hanzo default catalog (non-empty)")
}
}
+26
View File
@@ -0,0 +1,26 @@
package pricingsvc
import (
"os"
"strconv"
hplans "github.com/hanzoai/plans"
)
// loadPlansCatalog returns the @hanzo/plans catalog the pricing bundle reads
// for its subscription/blockchain/policy/tools/gpu endpoints. Sourced from the
// plans embed module so cloud has ONE copy of the plan catalog feeding both
// /v1/plans/* (plansvc) and /v1/pricing/{subscriptions,blockchain,…} (here).
func loadPlansCatalog() (map[string]any, error) {
return hplans.Data()
}
// parseFloatEnv reads a float env var with a default (markup knobs).
func parseFloatEnv(key string, dflt float64) float64 {
if v := os.Getenv(key); v != "" {
if f, err := strconv.ParseFloat(v, 64); err == nil {
return f
}
}
return dflt
}
+272
View File
@@ -0,0 +1,272 @@
// Package pricingsvc mounts the @hanzo/pricing service into the unified cloud
// binary under /v1/pricing/* (+ the /v1/models, /v1/gpu, /v1/tools aliases),
// per HIP-0106.
//
// HONEST GOJA STATUS: @hanzo/pricing is an EXPRESS app. Express needs Node's
// http/net stack and CANNOT run in goja. So the Express *transport* is dropped
// and replaced by native zip routes; the pricing *handlers* (pure transforms
// over data/pricing.json + the @hanzo/plans catalog) run in goja via the
// goja/bundle.js shipped by github.com/hanzoai/pricing. The sync.mjs MARKUP
// logic (toMTok/roundPrice/processOpenRouterModel/…) also runs in goja through
// the bundle's applyMarkup(); the only thing that does NOT run in goja is the
// live network fetch (OpenRouter/HuggingFace — no fetch/AbortController in
// goja), which this wrapper performs with Go's net/http and then feeds the raw
// JSON into applyMarkup. See SyncEnabled.
//
// Module boundary: pricing source + markup logic live in hanzoai/pricing. This
// wrapper is glue. No pricing data or markup math is reimplemented in Go.
//
// IAM gating + X-Org-Id: read endpoints are open to any authenticated caller
// (the public pricing catalog). The sync trigger is admin-only (c.IsAdmin()).
package pricingsvc
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/hanzoai/cloud"
"github.com/hanzoai/cloud/clients/gojahost"
hpricing "github.com/hanzoai/pricing"
"github.com/hanzoai/zip"
)
var host *gojahost.Host
// Mount registers the pricing surface on app per HIP-0106.
func Mount(app *zip.App, deps cloud.Deps) error {
if app == nil {
return fmt.Errorf("pricingsvc.Mount: nil zip.App")
}
logger := deps.Logger
if logger == nil {
return fmt.Errorf("pricingsvc.Mount: nil deps.Logger")
}
logger = logger.New("subsystem", "pricing")
bundle, err := hpricing.Bundle()
if err != nil {
return fmt.Errorf("pricingsvc.Mount: load bundle: %w", err)
}
pricingData, err := hpricing.Pricing()
if err != nil {
return fmt.Errorf("pricingsvc.Mount: load pricing.json: %w", err)
}
plansExtra, err := hpricing.PlansExtra()
if err != nil {
return fmt.Errorf("pricingsvc.Mount: load plans-extra: %w", err)
}
// The pricing bundle also reads the @hanzo/plans catalog for the
// subscription/blockchain/policy/tools/gpu endpoints. We pull that from the
// plans embed module so both subsystems share ONE source of truth.
plansData, err := loadPlansCatalog()
if err != nil {
return fmt.Errorf("pricingsvc.Mount: load plans catalog: %w", err)
}
h, err := gojahost.New(gojahost.Config{
Name: "pricing",
Bundle: bundle,
Globals: map[string]any{
"__PRICING_DATA__": pricingData,
"__PLANS_EXTRA__": plansExtra,
"__PLANS_DATA__": plansData,
"__MARKUP__": map[string]any{
"thirdParty": parseFloatEnv("THIRD_PARTY_MARKUP", 1.0),
"computeMonthly": parseFloatEnv("COMPUTE_MARKUP_MONTHLY", 1.0),
},
},
})
if err != nil {
return fmt.Errorf("pricingsvc.Mount: goja host: %w", err)
}
host = h
app.Get("/v1/pricing/health", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]any{"status": "ok", "service": "pricing"})
})
// /v1/pricing/* read surface (mirrors server.mjs handler-for-handler).
type binding struct{ path, route string }
fixed := []binding{
{"/v1/pricing", "pricing"},
{"/v1/pricing/models", "models"},
{"/v1/pricing/summary", "summary"},
{"/v1/pricing/free", "free"},
{"/v1/pricing/featured", "featured"},
{"/v1/pricing/compute", "compute"},
{"/v1/pricing/compute/presets", "compute/presets"},
{"/v1/pricing/cloud", "cloud"},
{"/v1/pricing/cloud/plans", "cloud/plans"},
{"/v1/pricing/cloud/regions", "cloud/regions"},
{"/v1/pricing/cloud/storage", "cloud/storage"},
{"/v1/pricing/providers", "providers"},
{"/v1/pricing/subscriptions", "subscriptions"},
{"/v1/pricing/blockchain", "blockchain"},
{"/v1/pricing/iam", "iam"},
{"/v1/pricing/base", "base"},
{"/v1/pricing/paas", "paas"},
{"/v1/pricing/policy", "policy"},
{"/v1/pricing/tools", "tools"},
{"/v1/pricing/gpu", "gpu"},
}
for _, b := range fixed {
route := b.route
app.Get(b.path, func(c *zip.Ctx) error { return dispatch(c, route, nil) })
}
// Single model lookup.
app.Get("/v1/pricing/model/:name", func(c *zip.Ctx) error {
return dispatch(c, "model", map[string]string{"name": c.Param("name")})
})
// Convenience aliases (the cleaner top-level surface from server.mjs).
app.Get("/v1/models", func(c *zip.Ctx) error { return dispatch(c, "__models_alias", nil) })
app.Get("/v1/pricing-policy", func(c *zip.Ctx) error { return dispatch(c, "policy", nil) })
// NOTE: /v1/plans, /v1/tools, /v1/gpu, /v1/cloud, /v1/subscriptions, /v1/iam
// are owned by plansvc / other subsystems at the top level to avoid route
// collisions; pricing serves them under its /v1/pricing/* prefix.
// Live sync trigger — admin only. Network fetch in Go, markup in goja.
app.Post("/v1/pricing/sync", func(c *zip.Ctx) error {
if !c.IsAdmin() {
return c.JSON(http.StatusUnauthorized, map[string]any{"error": "admin required"})
}
updated, err := RunSync(c.Context())
if err != nil {
c.Log().Error("pricing sync failed", "err", err)
return c.JSON(http.StatusInternalServerError, map[string]any{"error": "sync failed", "message": err.Error()})
}
return c.JSON(http.StatusOK, map[string]any{"status": "ok", "updated": updated})
})
logger.Info("pricing mounted",
"prefix", "/v1/pricing",
"routes", len(fixed)+4,
"express", false,
"goja", true,
"brand", deps.Brand,
)
return nil
}
func dispatch(c *zip.Ctx, route string, params map[string]string) error {
if host == nil {
return c.JSON(http.StatusServiceUnavailable, map[string]any{"error": "pricing not initialised"})
}
tenant := c.Org()
if tenant == "" {
tenant = "hanzo"
}
resp, err := host.Dispatch(c.Context(), gojahost.Request{Route: route, Params: params, Tenant: tenant})
if err != nil {
c.Log().Error("pricing dispatch failed", "route", route, "err", err)
return c.JSON(http.StatusInternalServerError, map[string]any{"error": "pricing dispatch failed"})
}
c.SetHeader("Content-Type", "application/json")
return c.Bytes(resp.Status, resp.Body)
}
// RunSync performs the live third-party model sync: fetch upstream listings
// (network — Go's net/http, since goja has no fetch), run the markup transform
// in goja via the bundle's applyMarkup(), and swap the shaped third-party
// section into the served catalog. Returns an ISO timestamp.
//
// This is the HONEST split: network IO in Go, markup math in JS. Only the
// dynamic third-party section is refreshed here; the Zen catalog + cloud/DO
// pricing in sync.mjs need the zen-gateway + DO credentials and stay on the
// standalone sync path for now.
func RunSync(ctx context.Context) (string, error) {
if host == nil {
return "", fmt.Errorf("pricing not initialised")
}
raw := map[string]any{}
// OpenRouter — public, no auth.
if or, err := fetchJSON(ctx, "https://openrouter.ai/api/v1/models"); err == nil {
if m, ok := or.(map[string]any); ok {
raw["openrouter"] = m["data"]
}
}
// (HuggingFace router needs a token; left to the standalone path unless
// HF_TOKEN is wired. We still call applyMarkup with whatever we fetched so
// the markup logic runs in goja.)
rawJSON, _ := json.Marshal(raw)
shapedJSON, err := host.Eval(ctx, "applyMarkup", rawJSON)
if err != nil {
return "", fmt.Errorf("applyMarkup: %w", err)
}
var shaped map[string]any
if err := json.Unmarshal(shapedJSON, &shaped); err != nil {
return "", fmt.Errorf("decode shaped: %w", err)
}
// Merge the freshly-shaped third-party section into the served catalog and
// re-inject so subsequent reads see it.
cur, _ := hpricing.Pricing()
if m, ok := cur.(map[string]any); ok {
if v, ok := shaped["thirdPartyModels"]; ok {
m["thirdPartyModels"] = v
}
if v, ok := shaped["providers"]; ok {
m["providers"] = v
}
if v, ok := shaped["freeModels"]; ok {
m["freeModels"] = v
}
ts := time.Now().UTC().Format(time.RFC3339)
m["updated"] = ts
host.SetGlobal("__PRICING_DATA__", m)
return ts, nil
}
return time.Now().UTC().Format(time.RFC3339), nil
}
func fetchJSON(ctx context.Context, url string) (any, error) {
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(cctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: status %d", url, resp.StatusCode)
}
b, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if err != nil {
return nil, err
}
var v any
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
return v, nil
}
func init() {
cloud.Register("pricing", 112, func(app any, deps cloud.Deps) error {
a, ok := app.(*zip.App)
if !ok {
return fmt.Errorf("pricingsvc.Mount: app is %T, want *zip.App", app)
}
return Mount(a, deps)
})
}
// Shutdown drops the goja host. Idempotent.
func Shutdown(context.Context) error {
if host == nil {
return nil
}
err := host.Close()
host = nil
return err
}
+161
View File
@@ -0,0 +1,161 @@
package pricingsvc
import (
"context"
"encoding/json"
"testing"
"github.com/hanzoai/cloud/clients/gojahost"
hplans "github.com/hanzoai/plans"
hpricing "github.com/hanzoai/pricing"
)
// newHost loads the REAL @hanzo/pricing goja bundle + embedded catalogs, so
// this exercises the actual server.mjs handler port + sync.mjs markup port
// running in goja (Express dropped).
func newHost(t *testing.T) *gojahost.Host {
t.Helper()
bundle, err := hpricing.Bundle()
if err != nil {
t.Fatalf("Bundle: %v", err)
}
pricingData, err := hpricing.Pricing()
if err != nil {
t.Fatalf("Pricing: %v", err)
}
plansExtra, err := hpricing.PlansExtra()
if err != nil {
t.Fatalf("PlansExtra: %v", err)
}
plansData, err := hplans.Data()
if err != nil {
t.Fatalf("plans.Data: %v", err)
}
h, err := gojahost.New(gojahost.Config{
Name: "pricing",
Bundle: bundle,
Globals: map[string]any{
"__PRICING_DATA__": pricingData,
"__PLANS_EXTRA__": plansExtra,
"__PLANS_DATA__": plansData,
"__MARKUP__": map[string]any{"thirdParty": 1.0, "computeMonthly": 1.0},
},
})
if err != nil {
t.Fatalf("gojahost.New: %v", err)
}
return h
}
func TestPricing_Summary(t *testing.T) {
h := newHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "summary"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
var body struct {
TotalModels int `json:"totalModels"`
ZenModels int `json:"zenModels"`
}
if err := json.Unmarshal(resp.Body, &body); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if body.TotalModels <= 0 || body.ZenModels <= 0 {
t.Fatalf("expected non-zero model counts, got total=%d zen=%d", body.TotalModels, body.ZenModels)
}
}
func TestPricing_PublicStripsInternal(t *testing.T) {
h := newHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "pricing"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
var body struct {
Cloud map[string]json.RawMessage `json:"cloud"`
}
if err := json.Unmarshal(resp.Body, &body); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if _, leaked := body.Cloud["_internal"]; leaked {
t.Fatal("cloud._internal (provider costs / routing) leaked into public /v1/pricing")
}
}
func TestPricing_Model404(t *testing.T) {
h := newHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "model", Params: map[string]string{"name": "no-such-model"}})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
if resp.Status != 404 {
t.Fatalf("status = %d, want 404", resp.Status)
}
}
func TestPricing_SubscriptionsFromPlans(t *testing.T) {
h := newHost(t)
defer h.Close()
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "subscriptions"})
if err != nil {
t.Fatalf("Dispatch: %v", err)
}
var body struct {
Plans []map[string]any `json:"plans"`
}
if err := json.Unmarshal(resp.Body, &body); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(body.Plans) == 0 {
t.Fatal("expected subscription plans from @hanzo/plans")
}
}
// TestPricing_ApplyMarkupRunsInGoja proves the sync.mjs markup transform runs
// in goja: feed a raw OpenRouter-shaped model, assert toMTok markup math.
func TestPricing_ApplyMarkupRunsInGoja(t *testing.T) {
h := newHost(t)
defer h.Close()
raw := `{"openrouter":[{"id":"openai/gpt-4o","name":"GPT-4o","context_length":128000,"pricing":{"prompt":"0.0000025","completion":"0.00001"}}],"huggingface":[{"id":"meta-llama/Llama-3.1-8B"}]}`
out, err := h.Eval(context.Background(), "applyMarkup", []byte(raw))
if err != nil {
t.Fatalf("Eval applyMarkup: %v", err)
}
var shaped struct {
ThirdPartyModels []struct {
ID string `json:"id"`
Pricing struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
} `json:"pricing"`
} `json:"thirdPartyModels"`
}
if err := json.Unmarshal(out, &shaped); err != nil {
t.Fatalf("unmarshal shaped: %v", err)
}
if len(shaped.ThirdPartyModels) != 2 {
t.Fatalf("expected 2 shaped models, got %d", len(shaped.ThirdPartyModels))
}
// toMTok: 0.0000025 * 1e6 * 1.0 = 2.5 ; 0.00001 * 1e6 = 10
var gpt4o *struct {
ID string `json:"id"`
Pricing struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
} `json:"pricing"`
}
for i := range shaped.ThirdPartyModels {
if shaped.ThirdPartyModels[i].ID == "openai/gpt-4o" {
gpt4o = &shaped.ThirdPartyModels[i]
}
}
if gpt4o == nil {
t.Fatal("gpt-4o not in shaped output")
}
if gpt4o.Pricing.Input != 2.5 || gpt4o.Pricing.Output != 10 {
t.Fatalf("markup math wrong: input=%v output=%v, want 2.5/10", gpt4o.Pricing.Input, gpt4o.Pricing.Output)
}
}
+3
View File
@@ -64,6 +64,9 @@ type rpcCommerce struct{ rpcEndpoint }
func (c *rpcCommerce) GetTenantConfig(_ context.Context, _ string) (*types.TenantConfig, error) {
return nil, c.errf("GetTenantConfig")
}
func (c *rpcCommerce) CheckEntitlement(_ context.Context, _, _ string) (*types.LicenseEntitlement, error) {
return nil, c.errf("CheckEntitlement")
}
type rpcAI struct{ rpcEndpoint }
-64
View File
@@ -1,64 +0,0 @@
// cloud-smoke is a minimal smoke harness for the cloud orchestrator
// HTTP+JSON path. It mounts a couple of in-process subsystems on a
// zip.App, brings up the listener, and exposes the /v1/base/health
// endpoint per the HIP-0106 reference contract. Used to verify the
// jsonv2 wiring end-to-end without pulling in the full subsystem
// matrix (which has unrelated build issues in cmd/cloud — see
// gateway/zap_wire.go uint16 overflow + gateway import cycle, both
// tracked separately).
package main
import (
"fmt"
"net/http"
"os"
"github.com/hanzoai/cloud"
"github.com/hanzoai/zip"
"github.com/hanzoai/zip/middleware"
)
func main() {
cfg := cloud.LoadConfig()
if cfg.DataDir == "" {
cfg.DataDir = "/tmp/cloud-smoke"
}
if cfg.Brand == "" {
cfg.Brand = "hanzo"
}
if cfg.Domain == "" {
cfg.Domain = "api.hanzo.ai"
}
deps := cloud.BuildDeps(cfg)
app := zip.New(zip.Config{Logger: deps.Logger})
app.Use(middleware.Recover())
app.Use(middleware.RequestID())
app.Use(middleware.Logger(deps.Logger))
// HIP-0106 reference health endpoints. The brief's smoke target.
app.Get("/v1/base/health", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]string{
"service": "base",
"status": "ok",
})
})
app.Get("/v1/vfs/health", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]string{
"service": "vfs",
"status": "ok",
})
})
deps.Logger.Info("cloud-smoke listening",
"http", cfg.ListenAddr,
"brand", cfg.Brand,
"domain", cfg.Domain,
"json_variant", zip.JSONVariant,
)
if err := app.Listen(cfg.ListenAddr); err != nil {
fmt.Fprintf(os.Stderr, "listen: %v\n", err)
os.Exit(1)
}
}
+11 -53
View File
@@ -4,6 +4,10 @@
// which subsystems mount at startup. Same artifact powers
// api.hanzo.ai, api.osage.cloud, api.lux.cloud, api.zoo.cloud, and
// every other white-label resold cloud surface.
//
// The serve body lives in cloud.Serve (one place, shared with the `hanzo`
// subcommand dispatcher); main() is just its full-surface entrypoint. The
// subsystem set is defined once in the subsystems bundle.
package main
import (
@@ -11,63 +15,17 @@ import (
"os"
"github.com/hanzoai/cloud"
"github.com/hanzoai/zip"
"github.com/hanzoai/zip/middleware"
// Subsystems — each ships func Mount(*zip.App, cloud.Deps) error and
// registers itself via init() in cloud.Registry. Import paths reflect
// where each subsystem's Mount lives.
_ "github.com/hanzoai/ai" // order 150
_ "github.com/hanzoai/amqp" // order 30
_ "github.com/hanzoai/authz" // order 70
_ "github.com/hanzoai/base" // order 60
_ "github.com/hanzoai/commerce" // order 100
_ "github.com/hanzoai/gateway" // order 80
_ "github.com/hanzoai/iam/pkg/iam" // order 50 (Mount lives in pkg/iam submodule)
_ "github.com/hanzoai/ingress" // order 90
_ "github.com/hanzoai/kms" // order 10
_ "github.com/hanzoai/mcp/go" // order 160 (Mount lives in go submodule)
_ "github.com/hanzoai/o11y" // order 70 (mounts alongside authz)
_ "github.com/hanzoai/vfs" // order 20
// Every subsystem registers into cloud.Registry via init(); the set is
// defined ONCE in the subsystems bundle (one source of truth, shared with
// cmd/hanzo). Blank-importing it populates the registry cloud.Serve mounts.
_ "github.com/hanzoai/cloud/subsystems"
)
func main() {
cfg := cloud.LoadConfig()
if err := cfg.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "config: %v\n", err)
os.Exit(1)
}
deps := cloud.BuildDeps(cfg)
app := zip.New(zip.Config{Logger: deps.Logger})
// Canonical middleware pipeline. Order matters:
// 1. Recover — panic → JSON 500
// 2. RequestID — generate / propagate X-Request-Id
// 3. Logger — request-line log via luxfi/log
// 4. Telemetry — OTel span; depends on deps.O11y if enabled
// 5. Auth — JWT validation; strips client identity, mints from JWT
app.Use(middleware.Recover())
app.Use(middleware.RequestID())
app.Use(middleware.Logger(deps.Logger))
// app.Use(middleware.Telemetry(deps.O11y)) // enable once o11y mounted
// app.Use(middleware.Auth(deps.IAM)) // enable once iam mounted
// Per-deployment subsystem mount.
if err := cloud.MountAll(app, cfg, deps); err != nil {
fmt.Fprintf(os.Stderr, "mount: %v\n", err)
os.Exit(1)
}
deps.Logger.Info("listening",
"http", cfg.ListenAddr,
"zap", cfg.ZAPListenAddr,
"brand", cfg.Brand,
"domain", cfg.Domain,
)
if err := app.Listen(cfg.ListenAddr); err != nil {
fmt.Fprintf(os.Stderr, "listen: %v\n", err)
// nil ⇒ honor cfg.Enable from flags/env (empty = all subsystems).
if err := cloud.Serve(nil); err != nil {
fmt.Fprintf(os.Stderr, "cloud: %v\n", err)
os.Exit(1)
}
}
+93
View File
@@ -0,0 +1,93 @@
package main
// Real integration tests for the unified Hanzo Cloud binary (HIP-0106).
// These exercise the actual orchestrator path — BuildDeps -> MountAll over the
// init()-populated Registry -> serve via the real zip/fiber + jsonenc stack —
// not a hand-rolled smoke harness. app.Fiber().Test drives requests in-process,
// no listener or external services.
import (
"net/http/httptest"
"testing"
"github.com/hanzoai/cloud"
"github.com/hanzoai/zip"
"github.com/hanzoai/zip/middleware"
)
// every subsystem main.go imports must self-register via init() — this is the
// proof the unified binary actually wires the whole matrix.
var wantSubsystems = []string{
"kms", "amqp", "metrics", "iam", "base", "authz", "o11y",
"gateway", "licensing", "plans", "pricing", "ai", "mcp",
}
func TestRegistryAssemblesSubsystems(t *testing.T) {
got := map[string]bool{}
for _, s := range cloud.Registry {
got[s.Name] = true
}
for _, name := range wantSubsystems {
if !got[name] {
t.Errorf("subsystem %q not registered — main.go import or its init() missing", name)
}
}
t.Logf("registry assembled %d subsystems", len(cloud.Registry))
}
// newTestApp mirrors main()'s wiring: BuildDeps + the canonical middleware
// pipeline + MountAll for the requested subsystems.
func newTestApp(t *testing.T, enable ...string) *zip.App {
t.Helper()
cfg := &cloud.Config{
Brand: "hanzo",
Domain: "api.hanzo.ai",
DataDir: t.TempDir(),
Enable: enable,
}
deps := cloud.BuildDeps(cfg)
app := zip.New(zip.Config{Logger: deps.Logger})
app.Use(middleware.Recover())
app.Use(middleware.RequestID())
app.Use(middleware.Logger(deps.Logger))
if err := cloud.MountAll(app, cfg, deps); err != nil {
t.Fatalf("MountAll(%v): %v", enable, err)
}
return app
}
// The self-contained subsystems mount in-process (per-tenant SQLite / in-mem,
// HIP-0302) and serve a healthy /v1/<name>/health with no external deps.
func TestMountAllAndServeHealth(t *testing.T) {
healthy := []string{"base", "authz", "amqp", "metrics", "plans", "pricing"}
app := newTestApp(t, healthy...)
for _, name := range healthy {
path := "/v1/" + name + "/health"
req := httptest.NewRequest("GET", path, nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
if resp.StatusCode != 200 {
t.Errorf("GET %s = %d, want 200", path, resp.StatusCode)
}
}
}
// Subsystems whose deps are disabled (no in-process peer, no ZAP endpoint) must
// mount and fail CLOSED — a 5xx from the disabled stub, never a panic or a
// silent 200. This proves the BuildDeps three-mode contract end-to-end.
func TestDepGatedSubsystemsFailClosed(t *testing.T) {
for _, name := range []string{"ai", "o11y"} {
app := newTestApp(t, name)
path := "/v1/" + name + "/health"
req := httptest.NewRequest("GET", path, nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
if resp.StatusCode < 500 {
t.Errorf("GET %s = %d, want >=500 (fail-closed; deps disabled)", path, resp.StatusCode)
}
}
}
+203
View File
@@ -0,0 +1,203 @@
// Command hanzo is the unified Hanzo Go binary, dispatched by subcommand.
//
// hanzo list subcommands
// hanzo --help list subcommands
// hanzo <svc> [flags] serve exactly one subsystem (iam, kms, commerce,
// gateway, ai, base, vfs, o11y, …)
// hanzo cloud [flags] serve the full unified surface (all enabled
// subsystems mounted into one zip.App / one listener)
//
// One binary. Many subsystems. The subcommand selects WHICH subsystem(s)
// serve this process; the same artifact is every standalone service AND
// the fused cloud control plane.
//
// Design — one mechanism, not many. Every Hanzo subsystem registers a
// cloud.MountSpec{Name, Order, Mount} into cloud.Registry via init() at
// package load (kms order 10, iam 50, gateway 80, commerce 100, …). A
// subcommand is therefore just a *selection* over that registry:
//
// - `hanzo <svc>` ⇒ cloud.Serve([]string{svc}); MountAll mounts only it.
// - `hanzo cloud` ⇒ cloud.Serve(nil); cfg.Enable per --enable (empty = all).
//
// Both paths run the identical compose root (BuildDeps → zip.App → health
// contract → MountAll → graceful Listen) — that body lives once in cloud.Serve
// and is shared with cmd/cloud. No subcommand duplicates boot logic.
//
// The single exception is `hanzo iam`. The registry's iam Mount (pkg/iam,
// order 50) wraps the Beego handler under /v1/iam/* for the fused surface;
// the FULL standalone IAM — login UI at /, all ~150 routes at root, LDAP +
// RADIUS listeners — is iamserver.Run(), the body of the legacy iamd
// main(). `hanzo iam` runs that, so the standalone identity provider is
// byte-for-byte what iamd shipped. See the iam case in dispatch().
//
// THE BEEGO CRUX (and why this binary does not init-panic). iam imports
// github.com/hanzoai/beego/v2; that fork carries process-global state
// (web.BeeApp singleton, ORM model registry, logger registration). The
// fear is that importing iam alongside the other subsystems collides at
// package load regardless of subcommand. It does not, for two reasons
// this codebase already established:
//
// 1. iam's ~150 route registrations and ORM table creation are NOT at
// package init() — they live inside iamserver.Init() / routers.InitAPI()
// / object.CreateTables(), which run only when iam actually serves.
// Blank-importing the package is inert: no router, no ORM, no listener.
// 2. There is exactly ONE Beego v2 import path in the graph
// (hanzoai/beego/v2), so there is exactly one Beego global to
// initialize, and it is initialized lazily by whichever path serves
// iam. (visor, the other Beego service, pins the *v1* fork
// github.com/beego/beego and is intentionally NOT linked here — two
// Beego majors in one binary is the collision to avoid, so we don't.)
//
// The proof is mechanical: the existing cmd/cloud binary already links
// this same graph (iam Beego v2 + kms + commerce + gateway + …) and builds
// + boots. cmd/hanzo links the same set plus iamserver for the standalone
// path; nothing new collides.
package main
import (
"fmt"
"os"
"sort"
"github.com/hanzoai/cloud"
// iamserver is the body of the standalone iamd main() — full Beego
// server (login UI, all routes, LDAP/RADIUS). `hanzo iam` calls Run().
"github.com/hanzoai/iam/iamserver"
// Every subsystem registers into cloud.Registry via init(); the set is
// defined ONCE in the subsystems bundle (shared with cmd/cloud), so the
// dispatcher and the full-surface binary mount an identical set. Inert at
// load — see THE BEEGO CRUX.
_ "github.com/hanzoai/cloud/subsystems"
)
// version is overridden at build time via -ldflags "-X main.version=...".
var version = "dev"
// nonRegistrySubcommands are the dispatch targets that do NOT correspond to
// a single cloud.Registry entry: the full fused surface, the standalone IAM
// boot, and the datastore (a ClickHouse C++ fork with no Go serve target —
// see the datastore case in dispatch()). Listed in --help alongside the
// registry-backed subcommands.
var nonRegistrySubcommands = map[string]string{
"cloud": "serve the full unified surface (all enabled subsystems, one listener)",
"iam": "serve standalone Hanzo IAM (full Beego server: login UI, OAuth2/OIDC, LDAP/RADIUS)",
"datastore": "ClickHouse-fork analytics DB — not a Go serve target (see help text)",
}
func main() {
if len(os.Args) < 2 {
usage(os.Stdout)
return
}
sub := os.Args[1]
switch sub {
case "-h", "--help", "help":
usage(os.Stdout)
return
case "version", "--version", "-v":
fmt.Printf("hanzo %s\n", version)
return
}
// Reset os.Args so the delegated service / cloud.LoadConfig sees its own
// flags at argv[1:], not the subcommand token. e.g. `hanzo kms --listen=:9000`
// → the kms serve path parses `--listen=:9000`.
os.Args = append(os.Args[:1], os.Args[2:]...)
if err := dispatch(sub); err != nil {
fmt.Fprintf(os.Stderr, "hanzo %s: %v\n", sub, err)
os.Exit(1)
}
}
// dispatch routes a subcommand to its serve entrypoint.
func dispatch(sub string) error {
switch sub {
case "cloud":
// Full fused surface: --enable governs the set (empty = all).
return cloud.Serve(nil)
case "iam":
// Standalone IAM = the body of iamd's main(). Full Beego server:
// login UI at /, ~150 routes at root, LDAP + RADIUS listeners,
// background sync loops. iamserver.Run() blocks until the process
// is signalled. This is intentionally NOT the /v1/iam/*-wrapped
// registry Mount — `hanzo iam` IS the identity provider, not a
// route-prefixed subsystem inside the cloud surface.
iamserver.Run()
return nil
case "datastore":
// Hanzo Datastore is a ClickHouse C++ fork. It has no Go
// Serve()/Run() to dispatch to: the server is the ClickHouse
// engine (built via CMake), and the only Go in the repo is
// cmd/zap-bridge — a SEPARATE per-package Go module
// (github.com/hanzoai/datastore/cmd/zap-bridge) built solely by
// the datastore Dockerfile's zap-builder stage, not part of this
// module graph. Folding it into `hanzo` would mean either cgo-
// linking ClickHouse into every Hanzo binary (a non-starter) or
// vendoring a second main module (violates one-binary). So
// datastore stays its own artifact; `hanzo datastore` documents
// that boundary instead of pretending to serve it.
return fmt.Errorf(
"datastore is a ClickHouse-fork analytics DB, not a Go serve target.\n" +
" - server: the ClickHouse engine (CMake build) — run its own image ghcr.io/hanzoai/datastore\n" +
" - zap-bridge: github.com/hanzoai/datastore/cmd/zap-bridge is a separate Go module,\n" +
" built only by the datastore Dockerfile; it is not linked into hanzo.\n" +
" use the standalone datastore deployment; `hanzo` composes the request-tier Go services")
default:
// Registry-backed single-service mode: serve exactly `sub`.
// Validate it is a known subsystem before booting anything.
if !registryHas(sub) {
usage(os.Stderr)
return fmt.Errorf("unknown subcommand %q", sub)
}
return cloud.Serve([]string{sub})
}
}
// registryHas reports whether name is a registered subsystem.
func registryHas(name string) bool {
for _, spec := range cloud.Registry {
if spec.Name == name {
return true
}
}
return false
}
// usage prints the subcommand list: the non-registry targets (cloud, iam,
// datastore) plus every subsystem registered into cloud.Registry, sorted.
func usage(w *os.File) {
fmt.Fprintf(w, "hanzo %s — the unified Hanzo Go binary\n\n", version)
fmt.Fprintf(w, "Usage:\n hanzo <subcommand> [flags]\n\n")
fmt.Fprintf(w, "Service subcommands:\n")
// Collect: registry names non-registry names, dedup, sort.
seen := map[string]string{}
for name, desc := range nonRegistrySubcommands {
seen[name] = desc
}
for _, spec := range cloud.Registry {
if _, ok := seen[spec.Name]; !ok {
seen[spec.Name] = fmt.Sprintf("serve the %s subsystem standalone", spec.Name)
}
}
names := make([]string, 0, len(seen))
for name := range seen {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Fprintf(w, " %-12s %s\n", name, seen[name])
}
fmt.Fprintf(w, "\nMeta:\n")
fmt.Fprintf(w, " %-12s %s\n", "help", "show this message")
fmt.Fprintf(w, " %-12s %s\n", "version", "print version and exit")
fmt.Fprintf(w, "\nFlags are per-subcommand (e.g. `hanzo cloud --enable=iam,kms --brand=hanzo`,\n")
fmt.Fprintf(w, "`hanzo kms --listen=:8443`). Run a subcommand to see its config via env/flags.\n")
}
+2 -2
View File
@@ -20,7 +20,7 @@ type Config struct {
// Domain is the deployment's primary public domain.
Domain string
// IAMIssuer is the JWKS issuer for JWT validation (usually iam.hanzo.id).
// IAMIssuer is the JWKS issuer for JWT validation (usually iam.hanzo.ai).
IAMIssuer string
// KMSMasterKeyRef points at the KMS master key for per-tenant DEK derivation.
@@ -74,7 +74,7 @@ func LoadConfig() *Config {
AdminListenAddr: getenv("CLOUD_ADMIN_LISTEN", ":8081"),
Brand: getenv("CLOUD_BRAND", "hanzo"),
Domain: getenv("CLOUD_DOMAIN", "api.hanzo.ai"),
IAMIssuer: getenv("CLOUD_IAM_ISSUER", "https://iam.hanzo.id"),
IAMIssuer: getenv("CLOUD_IAM_ISSUER", "https://iam.hanzo.ai"),
KMSMasterKeyRef: getenv("CLOUD_KMS_MASTER_KEY_REF", ""),
DataDir: getenv("CLOUD_DATA_DIR", "/var/lib/cloud"),
PaymentsZAPAddr: getenv("CLOUD_PAYMENTS_ZAP_ADDR", ""),
+36
View File
@@ -0,0 +1,36 @@
# Deploying hanzoai/cloud
Reference deployment manifests for the unified Hanzo Cloud binary
(HIP-0106). Each manifest demonstrates a different deployment topology.
| Manifest | Topology | Use case |
|----------|----------|----------|
| `compose.yml` | single-node Docker | VPS, dev, demo |
Coming soon: `kustomize/` and `helm/` for k8s deploys (see luxfi/operator
for the canonical CRD-driven shape).
## Quick start (Docker Compose)
```bash
cp deploy/compose.env.example deploy/compose.env
# edit HANZO_BRAND / HANZO_DOMAIN / HANZO_IAM_ISSUER as needed
docker compose -f deploy/compose.yml --env-file deploy/compose.env up -d
curl http://localhost:8080/health
```
## Environment
Required:
- `HANZO_IAM_ISSUER` — OIDC issuer URL. Without it the IAM subsystem
refuses to mount and the binary exits.
Optional (defaults shown):
- `HANZO_BRAND=hanzo`
- `HANZO_DOMAIN=api.hanzo.local`
- `HANZO_ENABLE=iam,base,kms,gateway,o11y`
- `HANZO_DATA_DIR=/var/lib/cloud`
The full subsystem list is in `cmd/cloud/main.go`. Per HIP-0106
payments and vault NEVER co-resident — leave them off this binary
unless you understand the PCI scope implications.
+19
View File
@@ -0,0 +1,19 @@
# Tag of ghcr.io/hanzoai/cloud to pull (defaults to :latest)
HANZO_CLOUD_TAG=latest
# White-label brand for this deployment
HANZO_BRAND=hanzo
# Public domain this binary serves (used to scope URLs in responses)
HANZO_DOMAIN=api.hanzo.ai
# OIDC issuer your IAM trusts (required — no default in the binary)
HANZO_IAM_ISSUER=https://iam.hanzo.id
# Subsystem mount list. Empty = mount everything imported in cmd/cloud/main.go.
HANZO_ENABLE=iam,base,kms,gateway,o11y
# Port mappings (override if host has conflicts)
HANZO_HTTP_PORT=8080
HANZO_METRICS_PORT=9090
HANZO_ZAP_PORT=9653
+48
View File
@@ -0,0 +1,48 @@
# Single-node VPS deployment of the unified hanzoai/cloud binary (HIP-0106).
#
# Mounts the iam + base + kms + gateway + o11y subsystems in-process.
# Payments / Vault stay out-of-process per HIP-0106 PCI scope; configure
# their ZAP endpoints via env if you need them.
#
# Usage:
# cp deploy/compose.env.example deploy/compose.env
# # edit HANZO_BRAND, HANZO_DOMAIN, HANZO_IAM_ISSUER
# docker compose -f deploy/compose.yml --env-file deploy/compose.env up -d
#
# Health check:
# curl http://localhost:8080/health
services:
cloud:
image: ghcr.io/hanzoai/cloud:${HANZO_CLOUD_TAG:-latest}
container_name: hanzo-cloud
restart: unless-stopped
ports:
- "${HANZO_HTTP_PORT:-8080}:8080"
- "${HANZO_METRICS_PORT:-9090}:9090"
- "${HANZO_ZAP_PORT:-9653}:9653"
environment:
HANZO_BRAND: ${HANZO_BRAND:-hanzo}
HANZO_DOMAIN: ${HANZO_DOMAIN:-api.hanzo.local}
HANZO_DATA_DIR: /var/lib/cloud
HANZO_IAM_ISSUER: ${HANZO_IAM_ISSUER:-https://iam.hanzo.id}
# Subsystem enable list (empty = all). Match what your tenant needs.
HANZO_ENABLE: ${HANZO_ENABLE:-iam,base,kms,gateway,o11y}
command:
- "--brand=${HANZO_BRAND:-hanzo}"
- "--domain=${HANZO_DOMAIN:-api.hanzo.local}"
- "--data-dir=/var/lib/cloud"
- "--iam-issuer=${HANZO_IAM_ISSUER:-https://iam.hanzo.id}"
- "--enable=${HANZO_ENABLE:-iam,base,kms,gateway,o11y}"
volumes:
- cloud-data:/var/lib/cloud
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
volumes:
cloud-data:
driver: local
+1
View File
@@ -86,6 +86,7 @@ type User = types.User
type Org = types.Org
type DBHandle = types.DBHandle
type TenantConfig = types.TenantConfig
type LicenseEntitlement = types.LicenseEntitlement
type ChatRequest = types.ChatRequest
type ChatResponse = types.ChatResponse
type Counter = types.Counter
+123 -187
View File
@@ -1,11 +1,16 @@
module github.com/hanzoai/cloud
go 1.26.3
go 1.26.4
// Dependencies will be added as subsystems are mounted per HIP-0106.
require (
github.com/hanzoai/zip v0.1.0
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
github.com/hanzoai/gateway v2.9.7+incompatible
github.com/hanzoai/iam v1.19.4
github.com/hanzoai/plans v1.2.0
github.com/hanzoai/pricing v1.3.0
github.com/hanzoai/zip v0.2.0
github.com/luxfi/log v1.4.3
)
@@ -13,25 +18,18 @@ require (
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/kms v1.25.0 // indirect
cloud.google.com/go/longrunning v0.8.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/pubsub v1.50.1 // indirect
cloud.google.com/go/pubsub/v2 v2.3.0 // indirect
cloud.google.com/go/storage v1.59.2 // indirect
cloud.google.com/go/trace v1.11.7 // indirect
contrib.go.opencensus.io/exporter/aws v0.0.0-20230502192102-15967c811cec // indirect
contrib.go.opencensus.io/exporter/jaeger v0.2.1 // indirect
contrib.go.opencensus.io/exporter/ocagent v0.7.0 // indirect
contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
contrib.go.opencensus.io/exporter/stackdriver v0.13.14 // indirect
contrib.go.opencensus.io/exporter/zipkin v0.1.2 // indirect
cloud.google.com/go/iam v1.7.0 // indirect
cloud.google.com/go/kms v1.27.0 // indirect
cloud.google.com/go/longrunning v0.9.0 // indirect
cloud.google.com/go/monitoring v1.25.0 // indirect
cloud.google.com/go/pubsub v1.50.2 // indirect
cloud.google.com/go/pubsub/v2 v2.5.1 // indirect
cloud.google.com/go/storage v1.61.3 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
filippo.io/hpke v0.4.0 // indirect
github.com/ALTree/bigfloat v0.2.0 // indirect
github.com/AfterShip/clickhouse-sql-parser v0.4.16 // indirect
@@ -39,7 +37,7 @@ require (
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1 // indirect
@@ -49,16 +47,13 @@ require (
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 // indirect
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
github.com/DataDog/opencensus-go-exporter-datadog v0.0.0-20220622145613-731d59e8b567 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/IBM/sarama v1.46.3 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/Machiel/slugify v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260311194731-d5b7577c683d // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20240116134246-a8cbe886bab0 // indirect
@@ -84,39 +79,34 @@ require (
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
github.com/aliyun/credentials-go v1.4.7 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apistd/uni-go-sdk v0.0.2 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
github.com/aws/aws-sdk-go v1.55.8 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.21 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aymerick/raymond v2.0.2+incompatible // indirect
github.com/baidubce/bce-sdk-go v0.9.260 // indirect
github.com/beego/beego/v2 v2.3.8 // indirect
github.com/beevik/etree v1.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
@@ -134,7 +124,6 @@ require (
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
@@ -146,16 +135,15 @@ require (
github.com/consensys/gnark-crypto v0.20.1 // indirect
github.com/corazawaf/coraza/v3 v3.3.3 // indirect
github.com/corazawaf/libinjection-go v0.2.2 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/corpix/uarand v0.2.0 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
github.com/cyphar/filepath-securejoin v0.5.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dghubble/oauth1 v0.7.3 // indirect
github.com/dghubble/sling v1.4.2 // indirect
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
@@ -172,13 +160,11 @@ require (
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/expr-lang/expr v1.17.7 // indirect
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect
@@ -198,11 +184,9 @@ require (
github.com/go-git/go-git/v5 v5.16.4 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-lark/lark v1.15.1 // indirect
github.com/go-ldap/ldap/v3 v3.4.6 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@@ -231,17 +215,15 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/webauthn v0.10.2 // indirect
github.com/go-webauthn/x v0.1.9 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gojek/heimdall/v7 v7.0.3 // indirect
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
@@ -249,7 +231,6 @@ require (
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
@@ -260,37 +241,43 @@ require (
github.com/google/go-tpm v0.9.8 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible // indirect
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/wire v0.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/gorilla/schema v1.4.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/gregdel/pushover v1.3.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.2.0 // indirect
github.com/hanzoai/authzstore v0.1.1 // indirect
github.com/hanzoai/beego/v2 v2.3.9 // indirect
github.com/hanzoai/builder v0.3.13 // indirect
github.com/hanzoai/common v0.67.7 // indirect
github.com/hanzoai/datastore-go/v2 v2.45.0 // indirect
github.com/hanzoai/dbx v1.16.0 // indirect
github.com/hanzoai/go-sms-sender v0.25.2 // indirect
github.com/hanzoai/gomail/v2 v2.3.2 // indirect
github.com/hanzoai/iam v1.18.0 // indirect
github.com/hanzoai/exporter-toolkit v0.15.2 // indirect
github.com/hanzoai/goauthorizenet v0.0.0-20180920213706-626992b83568 // indirect
github.com/hanzoai/gochimp3 v0.0.0-20241127054040-6051f77e24f1 // indirect
github.com/hanzoai/iamsdk/v2 v2.1.0 // indirect
github.com/hanzoai/idv v1.0.0 // indirect
github.com/hanzoai/kms/sdk/go v1.0.0 // indirect
github.com/hanzoai/kv-go/v9 v9.18.0 // indirect
github.com/hanzoai/ldapserver v1.2.1 // indirect
github.com/hanzoai/notify2 v1.6.3 // indirect
github.com/hanzoai/orm v0.5.1 // indirect
github.com/hanzoai/orm v0.5.2 // indirect
github.com/hanzoai/oss v1.8.5 // indirect
github.com/hanzoai/pubsub-go v1.0.0 // indirect
github.com/hanzoai/search-go v0.36.0 // indirect
github.com/hanzoai/sendgrid-go v3.4.2-0.20180724185151-733a05184a8d+incompatible // indirect
github.com/hanzoai/sigv4 v0.4.2 // indirect
github.com/hanzoai/storage-go v1.0.0 // indirect
github.com/hanzoai/tasks v1.40.0 // indirect
github.com/hanzoai/xorm v1.1.6 // indirect
@@ -312,6 +299,7 @@ require (
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/hashicorp/memberlist v0.5.4 // indirect
github.com/hashicorp/vault/api v1.14.0 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/hsluoyz/modsecurity-go v0.0.7 // indirect
github.com/huandu/go-sqlbuilder v1.35.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
@@ -334,14 +322,13 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/knadh/koanf v1.5.0 // indirect
github.com/knadh/koanf/v2 v2.3.2 // indirect
github.com/kpacha/opencensus-influxdb v0.0.0-20180520162117-1b490a38de4c // indirect
github.com/krakend/binder v0.0.0-20250826131726-e91a8a754ef8 // indirect
github.com/krakend/bloomfilter/v2 v2.1.0 // indirect
github.com/krakend/flatmap v1.2.0 // indirect
@@ -390,28 +377,43 @@ require (
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/luraproject/lura/v2 v2.12.1 // indirect
github.com/luxfi/accel v1.0.7 // indirect
github.com/luxfi/accel v1.1.9 // indirect
github.com/luxfi/address v1.0.1 // indirect
github.com/luxfi/age v1.5.0 // indirect
github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/compress v0.0.5 // indirect
github.com/luxfi/concurrent v0.0.3 // indirect
github.com/luxfi/consensus v1.22.85 // indirect
github.com/luxfi/consensus v1.25.0 // indirect
github.com/luxfi/constants v1.5.8 // indirect
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/crypto v1.19.0 // indirect
github.com/luxfi/corona v0.7.6 // indirect
github.com/luxfi/crypto v1.19.17 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/database v1.18.1 // indirect
github.com/luxfi/fhe v1.7.9 // indirect
github.com/luxfi/ids v1.2.9 // indirect
github.com/luxfi/kms v1.5.2 // indirect
github.com/luxfi/lattice/v7 v7.0.0 // indirect
github.com/luxfi/math v1.2.4 // indirect
github.com/luxfi/database v1.18.3 // indirect
github.com/luxfi/fhe v1.8.2 // indirect
github.com/luxfi/formatting v1.0.1 // indirect
github.com/luxfi/geth v1.16.100 // indirect
github.com/luxfi/go-bip32 v1.0.2 // indirect
github.com/luxfi/go-bip39 v1.1.2 // indirect
github.com/luxfi/ids v1.2.15 // indirect
github.com/luxfi/keys v1.1.0 // indirect
github.com/luxfi/kms v1.11.6 // indirect
github.com/luxfi/lattice/v7 v7.1.4 // indirect
github.com/luxfi/lens v0.1.4 // indirect
github.com/luxfi/math v1.4.1 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/mdns v0.1.0 // indirect
github.com/luxfi/metric v1.5.1 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/metric v1.5.8 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/ringtail v0.2.0 // indirect
github.com/luxfi/zap v0.3.1 // indirect
github.com/luxfi/zapdb v1.9.0 // indirect
github.com/luxfi/pq v1.0.3 // indirect
github.com/luxfi/proto v1.0.0 // indirect
github.com/luxfi/pulsar v1.1.1 // indirect
github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/threshold v1.9.4 // indirect
github.com/luxfi/tls v1.0.3 // indirect
github.com/luxfi/vm v1.2.0 // indirect
github.com/luxfi/zap v0.7.2 // indirect
github.com/luxfi/zapdb v1.10.0 // indirect
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/markbates/going v1.0.0 // indirect
@@ -426,6 +428,7 @@ require (
github.com/microsoft/go-mssqldb v1.9.5 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mileusna/viber v1.0.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.100 // indirect
@@ -438,28 +441,20 @@ require (
github.com/modelcontextprotocol/go-sdk v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/montanaflynn/stats v0.8.2 // indirect
github.com/montanaflynn/stats v0.9.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/nats-io/nats.go v1.50.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/netlify/netlify-go v0.1.11 // indirect
github.com/nyaruka/phonenumbers v1.2.2 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/open-feature/go-sdk v1.17.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 // indirect
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pariz/gountries v0.1.6 // indirect
github.com/paulmach/orb v0.13.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
@@ -468,21 +463,12 @@ require (
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/plaid/plaid-go/v15 v15.3.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/prometheus/alertmanager v0.31.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/exporter-toolkit v0.15.1 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/prometheus/prometheus v0.310.0 // indirect
github.com/prometheus/sigv4 v0.4.1 // indirect
github.com/prometheus/statsd_exporter v0.26.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
@@ -493,12 +479,11 @@ require (
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1 // indirect
github.com/redis/go-redis/v9 v9.17.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/resend/resend-go/v3 v3.1.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/rs/zerolog v1.35.0 // indirect
github.com/russellhaering/gosaml2 v0.11.0 // indirect
github.com/russellhaering/goxmldsig v1.6.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
@@ -528,6 +513,7 @@ require (
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/square/square-go-sdk/v3 v3.0.1 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.11.1 // indirect
@@ -540,8 +526,6 @@ require (
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tealeg/xlsx v1.0.5 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
github.com/thanhpk/randstr v1.0.4 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
@@ -557,10 +541,7 @@ require (
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/tmthrgd/go-memset v0.0.0-20190904060434-6fb7a21f88f1 // indirect
github.com/tmthrgd/go-popcount v0.0.0-20190904054823-afb1ace8b04f // indirect
github.com/twilio/twilio-go v1.13.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/unrolled/secure v1.15.0 // indirect
github.com/uptrace/bun v1.2.9 // indirect
@@ -572,7 +553,6 @@ require (
github.com/valyala/fastrand v1.1.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/volcengine/volc-sdk-golang v1.0.237 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
@@ -581,6 +561,7 @@ require (
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zap-proto/go v1.1.0 // indirect
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.mau.fi/util v0.8.3 // indirect
@@ -588,52 +569,25 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/collector/client v1.50.0 // indirect
go.opentelemetry.io/collector/component v1.51.0 // indirect
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
go.opentelemetry.io/collector/config/configretry v1.50.0 // indirect
go.opentelemetry.io/collector/confmap v1.51.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
go.opentelemetry.io/collector/consumer v1.51.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.144.0 // indirect
go.opentelemetry.io/collector/exporter v1.50.0 // indirect
go.opentelemetry.io/collector/exporter/exporterhelper v0.144.0 // indirect
go.opentelemetry.io/collector/extension v1.50.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.144.0 // indirect
go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
go.opentelemetry.io/collector/pdata v1.51.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.145.0 // indirect
go.opentelemetry.io/collector/pdata/xpdata v0.144.0 // indirect
go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0 // indirect
go.opentelemetry.io/collector/processor v1.51.0 // indirect
go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685 // indirect
go.opentelemetry.io/contrib/config v0.10.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.15.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
@@ -650,37 +604,35 @@ require (
gocloud.dev/secrets/hashivault v0.39.0 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/image v0.38.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/image v0.41.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/tools v0.45.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.275.0 // indirect
google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.62.0 // indirect
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apimachinery v0.35.0 // indirect
k8s.io/client-go v0.35.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
layeh.com/radius v0.0.0-20231213012653-1006025d24f8 // indirect
maunium.net/go/mautrix v0.22.1 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.50.0 // indirect
modernc.org/sqlite v1.51.0 // indirect
rsc.io/binaryregexp v0.2.0 // indirect
)
@@ -692,27 +644,28 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/hanzoai/ai v1.785.0
github.com/hanzoai/amqp v0.2.0
github.com/hanzoai/authz v1.10.0
github.com/hanzoai/base v1.3.1
github.com/hanzoai/commerce v1.37.0
github.com/hanzoai/gateway v0.2.0
github.com/hanzoai/iam/pkg/iam v1.18.0
github.com/hanzoai/authz v1.10.1
github.com/hanzoai/base v1.3.2
github.com/hanzoai/commerce v1.42.27
github.com/hanzoai/iam/pkg/iam v1.18.4
github.com/hanzoai/ingress v1.8.0
github.com/hanzoai/kms v0.159.0
github.com/hanzoai/kms v0.159.1
github.com/hanzoai/licensing v0.1.0
github.com/hanzoai/mcp/go v0.1.0
github.com/hanzoai/o11y v0.1.0
github.com/hanzoai/vfs v0.1.0
github.com/hanzoai/metrics v0.4.0
github.com/hanzoai/o11y v1.3.7
github.com/hanzoai/vfs v0.4.0
github.com/klauspost/compress v1.18.5 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.70.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
)
@@ -728,28 +681,11 @@ replace github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190
replace github.com/vulcand/oxy/v2 => github.com/traefik/oxy/v2 v2.0.0-20260126093803-fb11d60e0fdf
replace github.com/hanzoai/ai => ../ai
// Pin to last real go-sqlite3 release; v2.0.3+incompatible is a phantom (no module).
replace github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.16
replace github.com/hanzoai/amqp => ../amqp
replace github.com/prometheus/alertmanager => github.com/hanzoai/alertmanager v0.28.2
replace github.com/hanzoai/authz => ../authz
replace github.com/krakend/krakend-otel => github.com/hanzoai/krakend-otel v0.13.1
replace github.com/hanzoai/base => ../base
replace github.com/hanzoai/commerce => ../commerce
replace github.com/hanzoai/gateway => ../gateway
replace github.com/hanzoai/iam => ../iam
replace github.com/hanzoai/ingress => ../ingress
replace github.com/hanzoai/kms => ../kms
replace github.com/hanzoai/o11y => ../o11y
replace github.com/hanzoai/vfs => ../vfs
replace github.com/hanzoai/iam/pkg/iam => ../iam/pkg/iam
replace github.com/hanzoai/mcp/go => ../mcp/go
exclude github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83
+253 -878
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
.git/
.gitignore
*.tgz
README.md.tmpl
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v2
name: cloud
description: Hanzo Cloud — unified Go binary mounting iam/base/kms/gateway/o11y/commerce/ai (HIP-0106).
type: application
version: 0.1.0
appVersion: "0.1.0"
home: https://hanzo.ai
sources:
- https://github.com/hanzoai/cloud
maintainers:
- name: Hanzo
url: https://hanzo.ai
keywords:
- hanzo
- ai
- cloud
- unified-binary
- hip-0106
+30
View File
@@ -0,0 +1,30 @@
# helm/cloud
Minimal Helm chart for the unified `hanzoai/cloud` binary (HIP-0106).
## Install
```bash
helm install cloud ./helm/cloud \
--set hanzo.iamIssuer=https://iam.hanzo.id \
--set hanzo.brand=hanzo \
--set hanzo.domain=api.hanzo.ai \
--set image.tag=v0.1.0
```
## Required values
- `hanzo.iamIssuer` — OIDC issuer URL. Without this the IAM subsystem
refuses to mount and the pod CrashLoopBackOff's.
## What this chart is NOT
This is a single-Deployment / single-Service / single-ConfigMap chart.
For multi-tenant / multi-CRD operator-driven topology, install
`luxfi/operator` and use the `Service` CRD instead — that's the
canonical k8s shape per HIP-0106 + HIP-0014.
For PCI workloads (payments, vault) — those subsystems are NEVER
co-resident with the rest of cloud. Run them as separate Deployments
backed by their own charts; configure their ZAP RPC endpoints into
this chart's values.
+32
View File
@@ -0,0 +1,32 @@
{{/* Standard helpers */}}
{{- define "cloud.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "cloud.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- define "cloud.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "cloud.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "cloud.selectorLabels" -}}
app.kubernetes.io/name: {{ include "cloud.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
+65
View File
@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cloud.fullname" . }}
labels: {{- include "cloud.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels: {{- include "cloud.selectorLabels" . | nindent 6 }}
template:
metadata:
labels: {{- include "cloud.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets: {{- toYaml . | nindent 8 }}
{{- end }}
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: cloud
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- "--brand={{ .Values.hanzo.brand }}"
- "--domain={{ .Values.hanzo.domain }}"
- "--data-dir={{ .Values.hanzo.dataDir }}"
- "--iam-issuer={{ .Values.hanzo.iamIssuer }}"
- "--enable={{ .Values.hanzo.enable }}"
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
- name: zap
containerPort: 9653
protocol: TCP
livenessProbe: {{- toYaml .Values.probes.liveness | nindent 12 }}
readinessProbe: {{- toYaml .Values.probes.readiness | nindent 12 }}
securityContext: {{- toYaml .Values.securityContext | nindent 12 }}
resources: {{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: {{ .Values.hanzo.dataDir }}
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "cloud.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity: {{- toYaml . | nindent 8 }}
{{- end }}
+15
View File
@@ -0,0 +1,15 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "cloud.fullname" . }}-data
labels: {{- include "cloud.labels" . | nindent 4 }}
spec:
accessModes: {{- toYaml .Values.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "cloud.fullname" . }}
labels: {{- include "cloud.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- name: http
port: {{ .Values.service.httpPort }}
targetPort: http
protocol: TCP
- name: metrics
port: {{ .Values.service.metricsPort }}
targetPort: metrics
protocol: TCP
- name: zap
port: {{ .Values.service.zapPort }}
targetPort: zap
protocol: TCP
selector: {{- include "cloud.selectorLabels" . | nindent 4 }}
+75
View File
@@ -0,0 +1,75 @@
# hanzoai/cloud — Helm values
#
# Minimal chart: Deployment + Service + ConfigMap.
# Reference shape only — for richer ops use luxfi/operator and the
# Service CRD (operator wraps this shape with reconciliation).
image:
repository: ghcr.io/hanzoai/cloud
tag: "" # defaults to .Chart.AppVersion when empty
pullPolicy: IfNotPresent
imagePullSecrets: []
replicaCount: 1
# Brand / domain / enabled subsystems mirror the binary's CLI flags
# (cmd/cloud/main.go).
hanzo:
brand: hanzo
domain: api.hanzo.ai
dataDir: /var/lib/cloud
# OIDC issuer — REQUIRED. Without this the IAM mount refuses and the
# process exits at start.
iamIssuer: https://iam.hanzo.id
# Comma-separated subsystem list (empty = all imported in cmd/cloud).
# Payments + Vault are NEVER co-resident per HIP-0106 PCI isolation;
# leave them off unless you fully understand the implications.
enable: "iam,base,kms,gateway,o11y"
service:
type: ClusterIP
httpPort: 8080
metricsPort: 9090
zapPort: 9653
persistence:
# Per-tenant SQLite (HIP-0302) lives under dataDir. Set to an
# appropriate PVC for production; emptyDir is fine for dev.
enabled: false
size: 20Gi
storageClass: ""
accessModes:
- ReadWriteOnce
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
probes:
liveness:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
readiness:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# smoke-runtime.sh — boot the cloud binary and probe the mounted HTTP surface.
#
# Complements cmd/cloud-smoke (which exercises the in-process mount path).
# This script exercises the REAL ./cmd/cloud binary end-to-end: build, boot
# with the "default safe" --enable list, curl the endpoints that should return
# 200/401 per the HIP-0106 contract, kill the process, exit non-zero if any
# endpoint regresses.
#
# Intended for: CI smoke gating + local "does this clone actually serve?".
#
# Usage:
# scripts/smoke-runtime.sh # build, boot, probe, teardown
# PORT=8090 scripts/smoke-runtime.sh # alternate port
# KEEP_RUNNING=1 scripts/smoke-runtime.sh # leave the binary up after probes
# BIN=./bin/cloud scripts/smoke-runtime.sh # reuse an already-built binary
set -euo pipefail
PORT="${PORT:-8088}"
LISTEN="${LISTEN:-127.0.0.1:${PORT}}"
BIN="${BIN:-./bin/cloud}"
DATA_DIR="${DATA_DIR:-$(mktemp -d -t cloud-smoke.XXXXXX)}"
LOG_FILE="${LOG_FILE:-${DATA_DIR}/cloud-smoke.log}"
KEEP_RUNNING="${KEEP_RUNNING:-0}"
BOOT_TIMEOUT="${BOOT_TIMEOUT:-30}"
# IAM env vars are required by the kms subsystem even when iam is disabled,
# since kms validates inbound JWTs against the live hanzo.id IAM.
export IAM_URL="${IAM_URL:-https://hanzo.id}"
export IAM_ISSUER="${IAM_ISSUER:-https://hanzo.id}"
export IAM_AUDIENCE="${IAM_AUDIENCE:-kms}"
export IAM_KEYS_URL="${IAM_KEYS_URL:-https://hanzo.id/v1/iam/.well-known/jwks}"
# Default safe enable list — omits iam (panics on boot until hanzoai/iam ships
# the v1.19.2 fix for the orphan RunAuthzCommand route) and ingress/commerce
# (not yet wired into the default smoke set).
ENABLE="${ENABLE:-base,kms,gateway,o11y,mcp,ai,licensing,plans,pricing,amqp,metrics,vfs,authz}"
red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
log() { printf '[smoke-runtime] %s\n' "$*" >&2; }
cleanup() {
local rc=$?
if [[ "${KEEP_RUNNING}" != "1" ]]; then
if [[ -n "${CLOUD_PID:-}" ]] && kill -0 "${CLOUD_PID}" 2>/dev/null; then
log "stopping cloud (pid=${CLOUD_PID})"
kill "${CLOUD_PID}" 2>/dev/null || true
wait "${CLOUD_PID}" 2>/dev/null || true
fi
if [[ -d "${DATA_DIR}" && "${DATA_DIR}" == /tmp/cloud-smoke.* ]]; then
rm -rf "${DATA_DIR}"
fi
else
log "KEEP_RUNNING=1: cloud (pid=${CLOUD_PID:-?}) left up; logs at ${LOG_FILE}"
fi
exit "${rc}"
}
trap cleanup EXIT INT TERM
if [[ ! -x "${BIN}" ]]; then
log "building ${BIN}"
mkdir -p "$(dirname "${BIN}")"
go build -ldflags='-s -w' -o "${BIN}" ./cmd/cloud
fi
log "data dir: ${DATA_DIR}"
log "log file: ${LOG_FILE}"
log "starting: ${BIN} --brand=smoke --data-dir=${DATA_DIR} --enable=${ENABLE} --listen=${LISTEN}"
"${BIN}" \
--brand=smoke \
--domain=cloud.smoke.local \
--data-dir="${DATA_DIR}" \
--enable="${ENABLE}" \
--listen="${LISTEN}" \
> "${LOG_FILE}" 2>&1 &
CLOUD_PID=$!
# Wait for the listener to come up. We poll /healthz; if it doesn't bind
# within BOOT_TIMEOUT, dump the log tail and exit non-zero.
log "waiting for ${LISTEN} (timeout ${BOOT_TIMEOUT}s)…"
for i in $(seq 1 "${BOOT_TIMEOUT}"); do
if curl -fsS -o /dev/null "http://${LISTEN}/healthz" 2>/dev/null; then
log "listener up after ${i}s"
break
fi
if ! kill -0 "${CLOUD_PID}" 2>/dev/null; then
red "cloud exited before listener came up. Last log lines:"
tail -40 "${LOG_FILE}" >&2 || true
exit 1
fi
sleep 1
done
if ! curl -fsS -o /dev/null "http://${LISTEN}/healthz" 2>/dev/null; then
red "timed out waiting for /healthz on ${LISTEN}. Last log lines:"
tail -40 "${LOG_FILE}" >&2 || true
exit 1
fi
# Probe matrix. Format: "<expected_status> <path>".
# Values match the verified runtime contract per the HIP-0106 runbook:
# /healthz 200 — process health probe
# /v1/models 200 — catalog endpoint (no auth)
# /v1/plans 200 — plansvc catalog (goja-hosted)
# /v1/pricing 200 — pricingsvc catalog (goja-hosted)
# /v1/base/collections 401 — base subsystem alive, auth-gated
PROBES=(
"200 /healthz"
"200 /v1/models"
"200 /v1/plans"
"200 /v1/pricing"
"401 /v1/base/collections"
)
failed=0
for probe in "${PROBES[@]}"; do
expected="${probe%% *}"
path="${probe#* }"
actual="$(curl -sS -o /dev/null -w '%{http_code}' "http://${LISTEN}${path}" || echo 'ERR')"
if [[ "${actual}" == "${expected}" ]]; then
green " OK ${path} -> ${actual}"
else
red " FAIL ${path} -> expected=${expected} actual=${actual}"
failed=$((failed + 1))
fi
done
if [[ "${failed}" -gt 0 ]]; then
red "${failed} probe(s) failed. Tail of cloud log:"
tail -60 "${LOG_FILE}" >&2 || true
exit 1
fi
green "all ${#PROBES[@]} probes passed."
+93
View File
@@ -0,0 +1,93 @@
package cloud
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/hanzoai/zip"
"github.com/hanzoai/zip/middleware"
)
// Serve boots the canonical compose root and mounts the selected subsystems.
//
// This is the ONE place the cloud-server body lives. cmd/cloud (the full fused
// surface) and every `hanzo <svc>` subcommand share it; no boot logic is
// duplicated per entrypoint.
//
// enable==nil ⇒ honor cfg.Enable from flags/env (cloud mode; empty = all).
// enable!=nil ⇒ force exactly that set (single-service mode), overriding
// --enable so `hanzo kms` is unambiguous.
//
// Serve registers the HIP-0106 liveness contract (GET /v1/<name>/health for
// every enabled subsystem) before MountAll, runs the canonical middleware
// pipeline (Recover → RequestID → Logger), and shuts down gracefully on
// SIGINT/SIGTERM.
func Serve(enable []string) error {
cfg := LoadConfig()
if enable != nil {
cfg.Enable = enable
}
if err := cfg.Validate(); err != nil {
return fmt.Errorf("config: %w", err)
}
deps := BuildDeps(cfg)
app := zip.New(zip.Config{Logger: deps.Logger})
// Canonical middleware pipeline. Order matters:
// 1. Recover — panic → JSON 500
// 2. RequestID — generate / propagate X-Request-Id
// 3. Logger — request-line log
// Telemetry/Auth stay gateway-owned in Phase 1 (enable once mounted).
app.Use(middleware.Recover())
app.Use(middleware.RequestID())
app.Use(middleware.Logger(deps.Logger))
// HIP-0106 liveness contract: every enabled subsystem answers
// GET /v1/<name>/health uniformly, registered at the compose root before
// MountAll so it precedes subsystem /v1/<n>/* wildcards.
for _, spec := range Registry {
if !cfg.Enabled(spec.Name) {
continue
}
name := spec.Name
app.Get("/v1/"+name+"/health", func(c *zip.Ctx) error {
return c.JSON(200, map[string]string{"service": name, "status": "ok"})
})
}
if err := MountAll(app, cfg, deps); err != nil {
return fmt.Errorf("mount: %w", err)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
listenErr := make(chan error, 1)
go func() {
deps.Logger.Info("listening",
"http", cfg.ListenAddr,
"zap", cfg.ZAPListenAddr,
"enabled", cfg.Enable,
"brand", cfg.Brand,
"domain", cfg.Domain,
)
listenErr <- app.Listen(cfg.ListenAddr)
}()
select {
case <-ctx.Done():
deps.Logger.Info("shutdown requested")
case err := <-listenErr:
return fmt.Errorf("listen: %w", err)
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return app.ShutdownWithContext(shutdownCtx)
}
+39
View File
@@ -0,0 +1,39 @@
// Package subsystems is the single source of truth for which Hanzo cloud
// subsystems are linked into a binary.
//
// Blank-importing this package pulls every subsystem into the build graph;
// each one registers a cloud.MountSpec into cloud.Registry from its own
// init() (gated by //go:build cloud in the subsystem package, so the
// registrations fire only when built with -tags cloud).
//
// Both entrypoints — cmd/cloud (the full fused surface) and cmd/hanzo (the
// subcommand dispatcher) — blank-import THIS package and nothing else. The
// subsystem set is therefore defined ONCE, here; adding or removing a
// subsystem is a one-line change in one file, never duplicated per binary.
//
// (This package must NOT live in the root `cloud` package: the subsystems
// import `cloud` for Deps + Register, so a root-package bundle would form an
// import cycle. As a sibling subpackage it composes them without one.)
package subsystems
import (
_ "github.com/hanzoai/ai" // order 150
_ "github.com/hanzoai/amqp" // order 30
_ "github.com/hanzoai/authz" // order 70
_ "github.com/hanzoai/base" // order 60
_ "github.com/hanzoai/commerce" // order 100
_ "github.com/hanzoai/gateway" // order 80
_ "github.com/hanzoai/iam/pkg/iam" // order 50 (Mount lives in the pkg/iam submodule)
_ "github.com/hanzoai/ingress" // order 90
_ "github.com/hanzoai/kms" // order 10 (thin wrapper over the canonical luxfi/kms)
_ "github.com/hanzoai/licensing" // order 110 (after iam + commerce)
_ "github.com/hanzoai/mcp/go" // order 160 (Mount lives in the go submodule)
_ "github.com/hanzoai/metrics" // order 40
_ "github.com/hanzoai/o11y" // order 70
_ "github.com/hanzoai/vfs" // order 20
// Node-service subsystems hosted in-process via base+goja (HIP-0106);
// the JS + catalog data live in hanzoai/plans, hanzoai/pricing.
_ "github.com/hanzoai/cloud/clients/plansvc" // order 111 — /v1/plans/*
_ "github.com/hanzoai/cloud/clients/pricingsvc" // order 112 — /v1/pricing/*
)
+44
View File
@@ -46,6 +46,42 @@ type TenantConfig struct {
Brand string
}
// LicenseEntitlement is commerce's answer to "does this org/user hold an
// active entitlement for licensed product X, and what does its plan grant?".
//
// It is the inter-subsystem transport for the entitlement-flow that gates
// licensing token issuance (commerce → licensing → engine). The licensing
// subsystem copies Features verbatim into the signed token's `features`
// list so the proprietary engine's offline release gate (hasFeatures)
// enforces exactly the plan the buyer paid for.
//
// Features is the FLAT capability list produced from the canonical
// entitlement vocabulary by the data plane's toLicenseFeatures contract
// (@hanzo/plans entitlements.mjs): licensing.engine_features verbatim,
// plus derived capability tokens (e.g. "ai.premium", "training",
// "tools.<name>"), plus scoping tokens ("licensing.app:<id>",
// "licensing.product:<id>"). Numeric quotas (tokens_per_min, seats,
// max_vms, …) ride out of band and are NOT encoded here.
type LicenseEntitlement struct {
// ProductID is the licensed product the entitlement was checked for
// (e.g. "engine", "engine-rocm", a plugin id).
ProductID string
// Active reports whether the entitlement is currently valid (paid,
// not lapsed/cancelled). Licensing refuses to mint when false.
Active bool
// Plan is the resolved plan/tier id (e.g. "developer", "pro", "max",
// "enterprise"). Surfaced for logging/audit; not load-bearing for the
// release gate.
Plan string
// Features is the flat license-feature list per the toLicenseFeatures
// vocab contract — copied verbatim into License.Features at issue.
Features []string
// ExpiresUnix bounds the entitlement (unix seconds, 0 = no bound). The
// issued token's exp is clamped to it so a token never outlives the
// entitlement.
ExpiresUnix int64
}
// ChatRequest mirrors the AI subsystem's chat-completion request.
type ChatRequest struct {
Model string
@@ -116,6 +152,14 @@ type BaseClient interface {
// CommerceClient is the inter-subsystem interface to Commerce.
type CommerceClient interface {
GetTenantConfig(ctx context.Context, orgID string) (*TenantConfig, error)
// CheckEntitlement reports whether org `orgID` holds an active
// entitlement for licensed product `productID`, and returns the plan's
// flat license-features per the toLicenseFeatures vocab contract. Used
// by the licensing subsystem to gate + scope token issuance. orgID is
// the tenant the buyer acts as (X-Org-Id); when callers only have a
// user subject they pass it through here and commerce resolves the
// owning org.
CheckEntitlement(ctx context.Context, orgID, productID string) (*LicenseEntitlement, error)
}
// AIClient is the inter-subsystem interface to AI.