Compare commits

...
Author SHA1 Message Date
hanzo-dev 2a24345608 deps: bump ai -> 91659573 (complete per-org balance sweep)
release / build-amd64 (push) Failing after 26s
Folds in the zap-native + scraper per-org balance fixes so EVERY balance
check (gate, controller backstop, ZAP premium gate, zap balance query,
scraper preflight) reads the one per-org balance. Final image for the
per-org billing unification.
2026-06-23 21:33:13 -07:00
hanzo-dev d5b2e43e66 deps: bump ai -> e6402611 (per-org balance backstop)
release / build-amd64 (push) Failing after 1m8s
Completes the per-org billing unification: both the BalanceGateFilter AND the
resolveProviderForUser backstop now key by org slug + stamp X-Hanzo-Org, so the
single per-org credit is the balance every LLM call checks.
2026-06-23 21:27:09 -07:00
Hanzo 46b6f1d41d deps: bump ai -> ee423689 (zen→DO-AI routing + provider secret self-heal)
release / build-amd64 (push) Failing after 31s
Makes the LLM layer real:
- zen3/zen4/aliases re-pointed from dead Fireworks serverless to DO-AI
- do-ai provider key unified to kms://DO_AI_API_KEY (env-first resolution)
- provider re-seed self-heals ClientSecret/ProviderUrl/State on boot
2026-06-23 21:17:53 -07:00
hanzo-dev 0037bc76c7 build: resync luxfi go.sum to proxy/checksum-DB bits (fix re-tag drift)
release / build-amd64 (push) Failing after 32s
Several luxfi modules were force-rewritten upstream so go.sum captured the
rewritten direct-fetch bits, which conflict with the proxy's checksum-DB
artifacts: luxfi/age v1.5.0, luxfi/threshold v1.9.4, luxfi/zap v0.8.8.
Combined with the GOPROXY split (luxfi via proxy, hanzoai/zap-proto direct),
go.sum now pins the proxy/sumdb-authoritative hashes. luxfi stays in GONOSUMDB
so the few proxy-absent versions (e.g. luxfi/constants@v1.5.8 → 404) fall to
direct without a sumdb-lookup error while still pinned by go.sum.

Validated locally with the exact Dockerfile env: full 'go mod download' +
'CGO_ENABLED=0 go build ./cmd/cloud' succeed, 'go mod verify' = all modules
verified, binary embeds ai v1.785.9-...-44cd5f9a (per-org balance gate).
2026-06-23 21:12:47 -07:00
hanzo-dev 449c222834 build: split GONOPROXY/GONOSUMDB so luxfi/* resolves via proxy (fix age re-tag)
release / build-amd64 (push) Failing after 31s
GOPRIVATE forces BOTH direct-fetch and sumdb-bypass for every match, so
luxfi/age went direct to GitHub and hit the force-rewritten v1.5.0 tag
(h1:KEjq... != go.sum/sum.golang.org h1:G69H...), failing go mod download.
All luxfi/* modules we use are on the public proxy, so drop luxfi/* from
GONOPROXY (keep only hanzoai/* + zap-proto/*, whose just-pushed pseudo-
versions the proxy 404s). luxfi/* now resolves via proxy.golang.org =
immutable checksum-DB bits matching go.sum. Validated locally with the exact
Dockerfile env: luxfi/age proxy-clean, hanzoai/ai direct-clean.
2026-06-23 21:04:20 -07:00
hanzo-dev af0dba0709 build: proxy-first GOPROXY so force-rewritten upstream tags can't poison builds
release / build-amd64 (push) Failing after 35s
luxfi/age v1.5.0 was re-pushed on GitHub with content differing from the bits
sum.golang.org recorded (h1:G69H... original vs h1:KEjq... rewritten), so the
GOPRIVATE-forced direct fetch failed go.sum verification. Public luxfi/* are all
on the proxy; resolve through proxy.golang.org first (immutable, checksum-DB
artifacts) and fall back to direct only for repos the proxy 404s (private). Pins
public deps to verified bits; private resolution unchanged.
2026-06-23 20:58:16 -07:00
hanzo-dev 88229f300f deps: bump ai -> 44cd5f9a (per-org LLM balance gate)
release / build-amd64 (push) Failing after 33s
Unifies the LLM balance gate with commerce's per-org credit: the gate now
keys billing by org slug and stamps X-Hanzo-Org so a single per-org credit
(X-Org-Id=<org>) is the balance the gate checks and usage debits. Fixes
insufficient_balance on funded orgs (gate previously queried per-user in the
default 'hanzo' namespace).
2026-06-23 20:53:12 -07:00
hanzo-dev c870e8da01 deps: bump ai -> f3a36aa2 (iam SDK /v1/iam GetUrl + signout nil-guard)
release / build-amd64 (push) Failing after 39s
Fixes console2/cloud login end-to-end: the IAM SDK now loads the app cert from
/v1/iam/* so Signin's ParseJwtToken succeeds (was 'iamsdk: not valid PEM'),
establishing a real session that admin endpoints accept.
2026-06-22 01:01:45 -07:00
hanzo-dev 91bb757af0 build: drop re-added stale luxfi/threshold go.sum entry (re-tagged upstream)
release / build-amd64 (push) Failing after 32s
2026-06-21 20:55:49 -07:00
hanzo-dev 8817d2822c deps: bump hanzoai/dbx -> 6b6ceb7 (composite fields as JSON)
Fixes get-account 'unsupported type []model.SearchResult, a slice of struct'
and the matching scan errors — Message.SearchResults/VectorScores/Suggestions/
ToolCalls and all slice/map model fields now round-trip via JSON in the data
layer. Completes SQLite-native cloud-api login.
2026-06-21 20:54:03 -07:00
hanzo-dev 8a8bd51cb3 build: tolerate re-pushed private module tags (GOFLAGS=-mod=mod)
release / build-amd64 (push) Failing after 32s
luxfi/threshold@v1.9.4 is being re-tagged upstream, so its checksum drifts
from go.sum and 'go mod download' fails in CI. Record private-module hashes at
build time (-mod=mod; GOPRIVATE keeps them off the public sumdb) and drop the
stale threshold entry so it re-records cleanly.
2026-06-21 19:42:28 -07:00
hanzo-dev e103d951b0 deps: bump hanzoai/ai -> 152107f4 (dbx.Sync creates casibase schema on SQLite)
Unblocks the Base/SQLite cloud-api: the ai subsystem now creates its tables
from the Go structs on a fresh embedded SQLite store (no external migrations).
2026-06-21 19:37:14 -07:00
hanzo-dev 42aba75756 deps: bump hanzoai/ai -> v1.785.9-0...a98523d4 (StringList []string scan fix)
release / build-amd64 (push) Failing after 34s
Pins the merged ai main commit that adds StringList (sql.Scanner/Valuer over
JSON) for list columns — fixes the casibase data-layer panic
'unsupported Scan ... string into *[]string' that broke OAuth sign-in.
2026-06-21 19:15:26 -07:00
hanzo-dev 2aeba28467 deps: bump hanzoai/ai v1.785.7 -> v1.785.8 (CopyRequestBody for POST body parsing)
v1.785.8 sets beego CopyRequestBody in Bootstrap so the unified binary's AI
controllers can read POST bodies (json.Unmarshal of c.Ctx.Input.RequestBody);
without it /v1/chat/completions returned 'unexpected end of JSON input'. Completes
the unified-AI serve path: routing (bare /v1/*) + no-panic (session mgr) +
scratch-safe (memory sessions) + body parsing (CopyRequestBody).
2026-06-21 10:56:59 -07:00
hanzo-dev ad3a2fc688 deps: bump hanzoai/ai v1.785.6 -> v1.785.7 (memory session provider for scratch image)
v1.785.7 adds the scratch-safe memory session provider on top of the bare /v1/*
mount + session-manager build. Without it the unified binary 503'd on every
request (file session provider can't write in the read-only scratch root). With
all three fixes, /v1/chat/completions and the other OpenAI routes serve through
the unified binary.
2026-06-21 10:40:18 -07:00
hanzo-dev c771a04f0c fix(pricing): drop bare /v1/models alias so AI owns the OpenAI model list
In the unified binary the pricing subsystem (order 112) mounted a bare /v1/models
alias that shadowed the AI subsystem's (order 150) OpenAI-compatible /v1/models —
the {data:[{id,…}]} model list the api.hanzo.ai gateway forwards to cloud-api and
clients (cowork model picker) consume. Pricing's annotated catalog already lives
at /v1/pricing/models, so the bare alias only introduced a shape regression.
Remove it; pricing stays strictly under /v1/pricing/*. Now /v1/models, like the
other OpenAI routes, resolves to AI's beego handler via its /v1/* catch-all.
2026-06-21 10:32:25 -07:00
hanzo-dev 04b00a06a6 deps: bump hanzoai/ai v1.785.5 -> v1.785.6 (bare /v1/* mount + session manager)
v1.785.6 carries BOTH unified-binary fixes:
1. AI mounts casibase routes at bare /v1/* (not /v1/ai/*) so the api.hanzo.ai
   gateway, which forwards /v1/chat/completions etc. unchanged, resolves.
2. beego session manager built in Bootstrap so forwarded requests don't panic.

Together these make /v1/chat/completions, /v1/chat, /v1/models, /v1/messages
serve through the unified binary exactly as the gateway sends them.
2026-06-21 10:29:05 -07:00
hanzo-dev 670b526223 deps: bump hanzoai/ai v1.785.4 -> v1.785.5 (build beego session manager in Bootstrap)
ai v1.785.5 fixes the embedded /v1/ai/* HTTP 500: the unified binary never
calls beego.Run(), so beego.GlobalSessions was nil and every forwarded request
panicked in SessionStart. v1.785.5 builds the session manager in the shared
Bootstrap, so /v1/ai/chat/completions, /v1/ai/models and all nested routes
serve. Cloud binary builds green against it.
2026-06-21 10:14:16 -07:00
hanzo-dev 15d4859c07 ci(build): self-contained arcd build, GHCR login via GH_PAT
The ghcr.io/hanzoai/cloud package is linked to hanzoai/ai (cloud->ai rename),
so this repo GITHUB_TOKEN is denied write (permission_denied: write_package) via
the shared workflow. Build self-contained on the hanzo-build-linux-amd64 scale
set and log into GHCR with GH_PAT (admin:org+write:packages). gh_token still
feeds the Dockerfile private-module fetch. Dropped GHA cache (same denial +
artifact quota).
2026-06-21 09:31:43 -07:00
hanzo-dev 6653d1d23c deps: consume gateway/v2 v2.14.8 (proper /v2 module path)
gateways v2 tags were invalid Go modules (go.mod lacked the /v2 path), so
gateway v2.9.7+incompatible could not resolve on a clean fetch and cloud failed
to build. gateway v2.14.8 fixes the module path; import the /v2 path in the
subsystems bundle and pin v2.14.8. Build + go mod verify clean; binary boots
with no init panic.
2026-06-21 09:23:34 -07:00
hanzo-dev 7509d90a49 deps: pin gateway v2.9.6+incompatible (v2.9.7 added a go.mod without /v2 path)
gateway v2.9.7 introduced a go.mod still declaring module path
github.com/hanzoai/gateway at a v2 tag, which makes v2.9.7+incompatible an
invalid version (a module with a go.mod at major>=2 must use a /vN path).
v2.9.6 is the last go.mod-free v2 tag, so +incompatible is valid there; API is
identical for the subsystem blank-import. One patch down, no code change.
2026-06-21 09:10:37 -07:00
hanzo-dev f5e09f0f79 deps: refresh go.sum for force-pushed private tags (hanzoai/base v1.3.2, luxfi/threshold v1.9.4, ...)
Several private module tags were re-tagged after the committed go.sum was
generated, so a clean CI fetch failed go.sum verification (SECURITY ERROR:
checksum mismatch). Regenerated the affected private entries from current
remote content via go build -mod=mod (versions unchanged). Build is green and
boots without panic; go mod verify passes.
2026-06-21 09:03:59 -07:00
hanzo-dev b386c527d4 ci(build): route amd64 to ARC scale set hanzo-build-linux-amd64 by name
ARC ephemeral runners only match jobs targeting the scale-set name as a label.
The shared workflows default [self-hosted,linux,amd64] matches only classic
static runners (evo pool, offline) so the job sat queued with the listener
reporting assigned-job=0. gateways successful builds use runs-on:
hanzo-build-linux-amd64 (runner hanzo-build-linux-amd64-cvs28-runner-*); pass
that as runner-amd64.
2026-06-21 08:22:13 -07:00
hanzo-dev c60d398a54 ci(build): delegate to shared arcd docker-build.yml (self-hosted, billing-immune)
GitHub-hosted runners for this org are billing-frozen (jobs fail in ~5s:
"recent account payments have failed"), so the bespoke ubuntu-latest release
workflow can never start. Use the canonical hanzoai/.github docker-build.yml
reusable workflow, which runs on the self-hosted arcd pools and injects GH_PAT
as the gh_token BuildKit secret the Dockerfile needs for private cross-org Go
modules. amd64-only (cluster arch) to complete without the arm64 pool.
2026-06-21 08:09:19 -07:00
hanzo-dev de3f7c106d ci(build): authenticate private cross-org Go modules in image build
The unified binary pulls private hanzoai/* AND luxfi/* modules; the public
proxy 404s on them and the default GITHUB_TOKEN cannot read cross-org repos, so
the Docker build failed at go mod download.

- Dockerfile: split deps layer; GOPRIVATE + BuildKit gh_token secret +
  git insteadOf to fetch private modules over authenticated git (mirrors the
  proven hanzoai/ai Dockerfile). COPY --chmod=0755 the binary so the scratch
  image can never ship a non-executable /cloud (the 0644 CrashLoop class).
- release.yml: source the gh_token from the org GH_PAT (cross-org RO PAT that
  actually exists), not the never-configured HANZO_GH_RO_TOKEN.
2026-06-21 08:07:41 -07:00
hanzo-dev 2efdd3f46b deps: cut cloud-api onto unified binary — ai v1.785.4 (+#29 runtime init, /v1/billing path), beego v2.3.10 grace guard, go-openai fork
Fixes user-token AI on api.hanzo.ai:
- ai v1.785.4: AI runtime initializes in Mount() (#29) so /v1/ai/* serves real
  completions (no more 503 "ai runtime not initialized"); ai self-meters
  Commerce on the correct /v1/billing/* path (1.784.2 casibase used the dead
  /api/v1/billing/* -> 404 for real user JWTs, blocking the prepaid balance
  gate + usage auto-debit).
- beego v2.3.10: grace flag-registration guard so the unified binary (ai beego
  v1 + iam beego v2) does not panic ("flag redefined: graceful") at init.
- ai v1.785.4 also swaps deprecated denisenkom/go-mssqldb -> maintained
  microsoft/go-mssqldb (one mssql driver registration; no "sql.Register called
  twice" panic).
- replace sashabaranov/go-openai => hanzoai/go-openai v1.40.0 (ReasoningContent
  field) — mirrors ai own replace, which does not transit to this main module.

Verified: CGO_ENABLED=0 go build ./cmd/cloud produces a 316MB executable that
boots clean (base + full subsystem set) with no init panic; full boot stops
only on expected in-cluster config (IAM_KEYS_URL/IAM_AUDIENCE), supplied by the
cloud-api CR env.
2026-06-21 08:05:32 -07:00
z f7b364db90 deps: pin luxfi/kms v1.11.6 (published) — pkg/iam v1.18.5 referenced phantom v1.11.3 2026-06-19 01:15:08 -07:00
z f245d5a3da deps: pin goldap-free iam (pkg/iam v1.18.5, iam v1.19.6) — resell-clean (zero GPL-2.0) 2026-06-19 01:08:48 -07:00
antje 3b4ee86dce chore: add LICENSE (Apache-2.0) — Hanzo-native 2026-06-19 00:39:25 -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
38 changed files with 3586 additions and 776 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
+74
View File
@@ -0,0 +1,74 @@
name: release
# Builds and pushes ghcr.io/hanzoai/cloud:<tag> on tag (v*) / main / dispatch.
#
# Self-contained on the self-hosted arcd amd64 scale set — NEVER GitHub-hosted
# runners (this org's GitHub-hosted Actions are billing-frozen: jobs fail in ~5s
# with "recent account payments have failed").
#
# Why not the shared hanzoai/.github docker-build.yml: that workflow logs into
# GHCR with the repo's GITHUB_TOKEN, but the ghcr.io/hanzoai/cloud package is
# linked to a DIFFERENT repo (hanzoai/ai, from the cloud->ai module rename), so
# this repo's GITHUB_TOKEN is denied write to it (permission_denied:
# write_package). We log in with GH_PAT instead (admin:org + write:packages →
# writes any hanzoai package regardless of package-repo linkage), and pass it as
# the BuildKit gh_token the Dockerfile uses to fetch private cross-org Go
# modules. amd64-only: the cluster is amd64; pinning one platform completes on
# the live scale set without waiting on the arm64 pool.
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
packages: write
id-token: write
jobs:
build-amd64:
# ARC ephemeral runners match jobs targeting the scale-set NAME as a label.
runs-on: [hanzo-build-linux-amd64]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
driver-opts: network=host
- name: Log in to ghcr.io (GH_PAT — writes the cloud package despite its ai-repo linkage)
uses: docker/login-action@v3
with:
registry: ghcr.io
username: hanzo-dev
password: ${{ secrets.GH_PAT }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/hanzoai/cloud
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: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# gh_token: BuildKit secret the Dockerfile consumes to fetch private
# cross-org Go modules (hanzoai/*, luxfi/*) over authenticated git.
secrets: |
gh_token=${{ secrets.GH_PAT }}
+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
+49 -3
View File
@@ -1,16 +1,62 @@
# syntax=docker/dockerfile:1
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
# Module resolution is split by trust/availability, NOT lumped into one
# GOPRIVATE (which forces BOTH direct-fetch and sumdb-bypass for every match):
#
# GONOPROXY = hanzoai/* + zap-proto/* ONLY. These pseudo-versions are pinned
# to just-pushed commits the public proxy can't serve (404), so they must be
# fetched via authenticated git (gh_token rewrite below). luxfi/* is NOT
# here: most luxfi modules we use ARE on the public proxy, so they resolve
# through proxy.golang.org — the immutable, checksum-DB-verified artifact.
# That neutralises force-rewritten upstream tags: e.g. luxfi/age v1.5.0 and
# luxfi/threshold v1.9.4 were re-pushed on GitHub with content differing
# from what sum.golang.org recorded, which made the old direct fetch fail
# go.sum verification. Proxy fetch returns the original go.sum-matching bits,
# so the build is deterministic again. (go.sum is pinned to the proxy /
# checksum-DB hashes for these, NOT the rewritten GitHub bits.)
# GONOSUMDB = all three orgs: skip the public sumdb lookup for cross-org repos.
# This is REQUIRED for luxfi too — a few luxfi module versions are NOT on the
# proxy/sumdb (e.g. luxfi/constants@v1.5.8 → 404); GONOSUMDB lets those fall
# through to direct without a sumdb-lookup error, while go.sum still pins them.
# GOPROXY = proxy first, direct fallback. GONOPROXY still forces hanzoai/*
# and zap-proto/* to direct; everything else (incl. luxfi/*) tries the proxy
# first and only falls to direct when the proxy 404s.
#
# gh_token is the BuildKit secret the release workflow injects from GH_PAT;
# no-op when absent (local/dev builds with a warm module cache).
ENV GONOPROXY=github.com/hanzoai/*,github.com/zap-proto/*
ENV GONOSUMDB=github.com/hanzoai/*,github.com/luxfi/*,github.com/zap-proto/*
ENV GOPROXY=https://proxy.golang.org,direct
# -mod=mod lets `go` re-record go.sum entries at build time for private modules
# whose tags are re-pushed upstream (e.g. luxfi/threshold), instead of failing
# on a stale checksum. GONOSUMDB already keeps these off the public sumdb.
ENV GOFLAGS=-mod=mod
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
--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
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /cloud ./cmd/cloud
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -ldflags="-s -w" -o /cloud ./cmd/cloud
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /etc/passwd /etc/passwd
COPY --from=build /etc/group /etc/group
COPY --from=build /cloud /cloud
# --chmod=0755 guarantees the binary is executable in the scratch image. A prior
# build shipped its binary at mode 0644 -> "exec: permission denied" CrashLoop;
# be explicit so the unified binary can never regress to non-executable.
COPY --from=build --chmod=0755 /cloud /cloud
EXPOSE 8080 9090 9653
USER 65532:65532
ENTRYPOINT ["/cloud"]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+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
}
+277
View File
@@ -0,0 +1,277 @@
// 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).
// NOTE: the bare /v1/models alias is DELIBERATELY NOT mounted here. In the
// unified binary the AI subsystem owns the OpenAI-compatible /v1/models
// (the {data:[{id,…}]} model list the api.hanzo.ai gateway forwards and
// clients like cowork's model picker consume). Pricing's annotated catalog
// already lives at /v1/pricing/models, so the bare alias would only shadow
// AI's contract route with a different shape — a regression. Keep pricing
// strictly under /v1/pricing/*. (Same reasoning the note below records for
// /v1/plans, /v1/tools, /v1/gpu, /v1/cloud, /v1/subscriptions, /v1/iam —
// all owned by other subsystems at the top level to avoid collisions.)
app.Get("/v1/pricing-policy", func(c *zip.Ctx) error { return dispatch(c, "policy", nil) })
// 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
+246 -197
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 v2.14.8
github.com/hanzoai/iam v1.19.8-0.20260622075908-c2fd4872545b
github.com/hanzoai/plans v1.2.0
github.com/hanzoai/pricing v1.3.0
github.com/hanzoai/zip v0.2.1
github.com/luxfi/log v1.4.3
)
@@ -13,25 +18,25 @@ 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 v1.58.0 // 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/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.59.2 // indirect
cloud.google.com/go/trace v1.11.7 // indirect
cloud.google.com/go/trace v1.12.0 // 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
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,13 +44,14 @@ 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
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 // indirect
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
github.com/Azure/go-amqp v1.5.1 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect
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
@@ -58,66 +64,80 @@ require (
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
github.com/SherClockHolmes/webpush-go v1.4.0 // indirect
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd // indirect
github.com/SigNoz/signoz-otel-collector v0.144.2 // indirect
github.com/ThinkInAIXYZ/go-mcp v0.2.24 // indirect
github.com/WqyJh/go-cosyvoice v0.1.0 // indirect
github.com/WqyJh/go-openai-realtime v0.5.1-0.20250210083616-024eddd5a481 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/cloudauth-20190307/v3 v3.9.2 // indirect
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 // indirect
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/ecs-20140526/v4 v4.26.10 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
github.com/alibabacloud-go/openapi-util v0.1.2 // indirect
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
github.com/alibabacloud-go/resourcecenter-20221201 v1.5.1 // indirect
github.com/alibabacloud-go/tea v1.4.0 // indirect
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
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-utils/v2 v2.0.9 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
github.com/alibabacloud-go/vod-20170321/v2 v2.16.10 // indirect
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 // 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/anthropics/anthropic-sdk-go v1.4.0 // 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/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 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // 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/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0 // 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/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.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.99.0 // 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.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // 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/baidubce/bce-qianfan-sdk/go/qianfan v0.0.14 // indirect
github.com/baidubce/bce-sdk-go v0.9.264 // indirect
github.com/beego/beego v1.12.12 // 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/blang/semver/v4 v4.0.0 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/btcsuite/btcd v0.25.0 // indirect
@@ -125,11 +145,14 @@ require (
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/btcsuite/btclog v1.0.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/bwmarrin/discordgo v0.28.1 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
github.com/carapace-sh/carapace-shlex v1.0.1 // indirect
github.com/carmel/gooxml v0.0.0-20220216072414-40ff56130850 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -138,32 +161,44 @@ require (
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
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/coder/quartz v0.3.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/cohere-ai/cohere-go/v2 v2.5.2 // indirect
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 // indirect
github.com/consensys/gnark-crypto v0.20.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // 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/crate-crypto/go-eth-kzg v1.5.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/cyphar/filepath-securejoin v0.6.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/deckarep/golang-set/v2 v2.8.0 // 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
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/di-wu/parser v0.2.2 // indirect
github.com/di-wu/xsd-datetime v1.0.0 // indirect
github.com/digitalocean/go-libvirt v0.0.0-20250207191401-950a7b2d7eaf // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/diskfs/go-diskfs v1.2.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
@@ -172,33 +207,36 @@ 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/emicklei/go-restful/v3 v3.13.0 // 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/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // 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
github.com/fogleman/gg v1.3.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gage-technologies/mistral-go v1.1.0 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/gin-gonic/gin v1.12.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
github.com/go-acme/lego/v4 v4.32.0 // indirect
github.com/go-acme/lego/v4 v4.34.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.4 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.1 // 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-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-lark/lark v1.15.1 // indirect
github.com/go-ldap/ldap/v3 v3.4.6 // indirect
@@ -231,17 +269,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-sql-driver/mysql v1.9.3 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-sql-driver/mysql v1.10.0 // 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,48 +285,54 @@ 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
github.com/google/cel-go v0.26.1 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
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-20260302011040-a15ffb7f9dcc // 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.10 // indirect
github.com/hanzoai/builder v0.3.13 // indirect
github.com/hanzoai/common v0.67.7 // indirect
github.com/hanzoai/dashscope-go-sdk v0.0.2 // indirect
github.com/hanzoai/dashscopego v0.6.0 // 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/dbx v1.16.1-0.20260622034226-6b6ceb740f3e // indirect
github.com/hanzoai/exporter-toolkit v0.15.2 // indirect
github.com/hanzoai/go-openrouter v1.0.0 // 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/oss v1.8.5 // indirect
github.com/hanzoai/pdf v1.2.0 // indirect
github.com/hanzoai/pubsub-go v1.0.0 // indirect
github.com/hanzoai/search-go v0.36.0 // 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
@@ -306,15 +348,21 @@ require (
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
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/henomis/lingoose v0.1.0 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/pkcs7 v0.2.0 // indirect
github.com/hhrutter/tiff v1.0.2 // 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
github.com/hupe1980/go-huggingface v0.0.15 // indirect
github.com/icrowley/fake v0.0.0-20240710202011-f797eb4a99c0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/influxdata/influxdb v1.11.6 // indirect
@@ -330,11 +378,11 @@ require (
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
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/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
@@ -382,42 +430,62 @@ require (
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx v1.2.29 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lib/pq v1.12.1 // indirect
github.com/leverly/ChatGLM v1.2.0 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/likexian/gokit v0.25.13 // indirect
github.com/likexian/whois v1.15.1 // indirect
github.com/likexian/whois-parser v1.24.9 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/lithammer/shortuuid v3.0.0+incompatible // indirect
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/luthermonson/go-proxmox v0.2.1 // indirect
github.com/luxfi/accel v1.2.4 // 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.20 // 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.17.5 // 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.8.8 // indirect
github.com/luxfi/zapdb v1.10.0 // indirect
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/markbates/going v1.0.0 // indirect
github.com/markbates/goth v1.82.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
@@ -426,6 +494,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
@@ -435,14 +504,15 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mmcdole/gofeed v1.2.1 // indirect
github.com/mmcdole/goxpp v1.1.0 // indirect
github.com/modelcontextprotocol/go-sdk v1.4.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modelcontextprotocol/go-sdk v1.4.1 // 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // 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
@@ -453,84 +523,87 @@ require (
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/openai/openai-go/v2 v2.1.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
github.com/oschwald/geoip2-golang v1.11.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/pariz/gountries v0.1.6 // indirect
github.com/paulmach/orb v0.13.0 // indirect
github.com/pdfcpu/pdfcpu v0.11.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.7 // 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
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1 // indirect
github.com/redis/go-redis/v9 v9.17.2 // indirect
github.com/redis/go-redis/v9 v9.20.0 // 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
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/samber/lo v1.47.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sashabaranov/go-openai v1.32.0 // indirect
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/segmentio/analytics-go/v3 v3.2.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/segmentio/encoding v0.5.3 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sethvargo/go-password v0.2.0 // indirect
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/slack-go/slack v0.15.0 // indirect
github.com/slack-go/slack v0.23.1 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/speps/go-hashids v2.0.0+incompatible // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // 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
github.com/subosito/gotenv v1.6.0 // indirect
github.com/supranational/blst v0.3.16 // indirect
github.com/swaggest/jsonschema-go v0.3.78 // indirect
github.com/swaggest/openapi-go v0.2.60 // indirect
@@ -540,8 +613,10 @@ 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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.77 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm v1.0.1116 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/hunyuan v1.0.1074 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tbaas v1.0.1115 // 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 +632,8 @@ 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/ua-parser/uap-go v0.0.0-20251207011819-db9adb27a0b8 // 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,70 +645,48 @@ 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/volcengine/volc-sdk-golang v1.0.241 // indirect
github.com/volcengine/volcengine-go-sdk v1.0.141 // indirect
github.com/wangbin/jiebago v0.3.2 // indirect
github.com/workweixin/weworkapi_golang v0.0.0-20200831071321-c1fdfd3d6e7d // 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
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.1-0.20200424115421-065759f9c3d7 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
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
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
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/detectors/gcp v1.42.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 v1.44.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/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
@@ -649,39 +700,53 @@ require (
gocloud.dev/pubsub/rabbitpubsub v0.37.0 // indirect
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/exp v0.0.0-20260410095643-746e56fc9e2f // 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/sync v0.21.0 // indirect
golang.org/x/term v0.44.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
google.golang.org/grpc v1.80.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.275.0 // indirect
google.golang.org/genai v1.10.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.81.1 // 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/djherbis/times.v1 v1.2.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // 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/api v0.35.0 // 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/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/metrics v0.30.0 // 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
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.20.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
require (
@@ -690,29 +755,30 @@ require (
github.com/gofiber/schema v1.7.1 // indirect
github.com/gofiber/utils/v2 v2.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hanzoai/ai v1.785.0
github.com/hanzoai/ai v1.785.9-0.20260624043121-916595739e84
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.5
github.com/hanzoai/iam/pkg/iam v1.18.5
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.53.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
)
@@ -728,28 +794,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
replace github.com/sashabaranov/go-openai => github.com/hanzoai/go-openai v1.40.0
+661 -457
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/v2" // 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.