Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a24345608 | ||
|
|
d5b2e43e66 | ||
|
|
46b6f1d41d | ||
|
|
0037bc76c7 | ||
|
|
449c222834 | ||
|
|
af0dba0709 | ||
|
|
88229f300f | ||
|
|
c870e8da01 | ||
|
|
91bb757af0 | ||
|
|
8817d2822c | ||
|
|
8a8bd51cb3 | ||
|
|
e103d951b0 | ||
|
|
42aba75756 | ||
|
|
2aeba28467 | ||
|
|
ad3a2fc688 | ||
|
|
c771a04f0c | ||
|
|
04b00a06a6 | ||
|
|
670b526223 | ||
|
|
15d4859c07 | ||
|
|
6653d1d23c | ||
|
|
7509d90a49 | ||
|
|
f5e09f0f79 | ||
|
|
b386c527d4 | ||
|
|
c60d398a54 | ||
|
|
de3f7c106d | ||
|
|
2efdd3f46b | ||
|
|
f7b364db90 | ||
|
|
f245d5a3da | ||
|
|
3b4ee86dce | ||
|
|
d77e554b7e | ||
|
|
e075f7a438 | ||
|
|
cea54a96b0 | ||
|
|
ae8c0cea98 | ||
|
|
14bda07881 | ||
|
|
4de40e2396 | ||
|
|
7f1e0d61df | ||
|
|
18738d56b2 | ||
|
|
942feca4d7 | ||
|
|
ad7828553e | ||
|
|
08a6ef73e7 | ||
|
|
4e17df928d | ||
|
|
223fc137f3 | ||
|
|
8ca298873c | ||
|
|
b90e629308 | ||
|
|
7876d86241 | ||
|
|
91970431f3 | ||
|
|
50add91a84 | ||
|
|
ea1a699d9e | ||
|
|
10ad830e10 | ||
|
|
58bb1843a4 | ||
|
|
bb2600bd9b | ||
|
|
5c6beb850b | ||
|
|
1e1e017ee4 | ||
|
|
991ec0e788 | ||
|
|
8b59797b59 | ||
|
|
e67bd8aa6d | ||
|
|
ee879c028b | ||
|
|
e0680a7030 | ||
|
|
952a5112b3 | ||
|
|
d60e476ea8 | ||
|
|
66574461da |
@@ -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
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
*.tgz
|
||||
README.md.tmpl
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
Executable
+137
@@ -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."
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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/*
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user