Deploy
Package deploy is Hanzo CD: see what each app is running, sync it, and roll back a bad release.
Package deploy is Hanzo CD: see what each app is running, sync it, and roll back a bad release.
| Base URL | https://api.hanzo.ai |
| Operations | 20 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1122 · Deploy — The GitOps Plane — Draft · read the specification →
/v1/deploy is Hanzo CD: see what each app in the fleet is running, sync it,
and roll back a bad release. It is implemented in hanzoai/cloud at
apps/deploy, observing the operator-managed fleet — applications, resource
tree, per-object health, live diff — and driving reconciliation. Each operator
hanzo.ai/v1 App CR is a GitOps Application (apps/deploy/deploy.go:8-11).
This HIP states that the cluster is the store, which reads are tenant-scoped,
and why the writes are SuperAdmin-only.
Motivation
The operator reconciles declared state into workloads whether or not anyone is
watching; what was missing was the watch. Without one plane that projects the
fleet — declared version, health, sync, the owned-resource tree — the answer to
"what is running" is a kubectl session, which neither the console nor a tenant
can be handed. This plane is that projection, at the addresses the CD dashboard
that consumes it already speaks (apps/deploy/deploy.go:13-15).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The cluster is the store
deploy owns no store. Its state is the cluster's: App CRs, their owned objects
(Deployment, Service, Ingress, ReplicaSet, Pod), and the static-plane site CRDs
projected as Application rows so the fleet list covers the whole delivery
surface (apps/deploy/deploy.go:76-84). Nothing this plane serves survives the
cluster it reads, and nothing needs to.
§2 The addresses
Every route is under /v1/deploy, enumerated as explicit prefixes in the
manifest row (manifest/apps.go:175). The fleet reads — applications, detail,
resource tree, revisions, clusters, projects, settings, gitops, version — are
typed. The raw routes are raw because their answers cannot be values:
stream/applications and the streamed resource tree are event streams;
login, callback and logout are the console's OIDC redirect dance against
IAM (apps/deploy/login.go:128-140); sync, rollback and reconcile are
imperatives answering acknowledgement, not a resource; account/can-i/{...} is
a wildcard permission probe the dashboard issues.
§3 Tenancy — two scopes, one predicate
Scope is derived from the validated identity in one place
(resolveScope, apps/deploy/scope.go:66-84): a SuperAdmin — decided by the
c.IsAdmin() predicate alone, which already implies a validated principal —
sees and mutates the whole fleet; a validated org member sees only apps carrying
its own hanzo.ai/org label, read-only. The writes (sync, rollback, reconcile)
MUST remain SuperAdmin-only: they change what runs. Secret objects are never
surfaced — no tree node, no manifest — so the projection cannot leak
materialized env (apps/deploy/deploy.go:36-38). Console sign-in resolves
SuperAdmins through the admin-console IAM application, whose organization is
the reserved admin org (apps/deploy/login.go:101-106), and validates tokens
with the same validator the identity boundary uses.
§4 Money, events, telemetry
deploy is free, in those words (plugin/deploy/main.go:21, cloud.Free; not
in spend.go:275). It publishes no events on the bus — the stream routes are
per-request server-sent events, not bus topics — and it emits nothing to
observability beyond the request span every route gets.
§5 Stage
deploy is ga: it is the platform core's delivery lens, part of the
self-service agentic OS rather than a vertical application.
§6 Upstream
deploy derives from no forked code. Two third-party facts stand: it reads the
cluster through k8s.io/client-go (Apache-2.0), and it serves the CD
dashboard's own address shapes so that console consumes it unchanged
(apps/deploy/deploy.go:13-15) — a wire dialect implemented, not code
inherited.
Rationale
The alternative to reading the cluster is keeping a deployment database and reconciling it against reality — a second copy of state whose one failure mode, drift, is exactly what a CD plane exists to expose. Reading the CRs directly means the plane can be wrong only by being stale, never by disagreeing.
Security Considerations
The wrong implementation is a cluster console handed to tenants. Three
boundaries hold it: writes require the SuperAdmin predicate, org reads are
filtered by the org label resolved from the validated principal (never a
header), and Secrets are excluded from the tree at the GVR list, so no query
shape can reach them. The OIDC flow fails closed — a missing verifier or public
URL disables sign-in rather than weakening it (apps/deploy/login.go:119-121).
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | deploy at its own prefix | 20 operations |
| CLI | hanzo deploy … | 20 of 20 |
| SDK | DeployApi in every published client | 18 of 20 — the clients are generated at their own release |
| MCP | tool deploy on https://api.hanzo.ai/v1/mcp | 21 operations, 5 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/deploy/login, operation get_deploy_login:
hanzo deploy loginimport { Configuration, DeployApi } from 'hanzoai';
const api = new DeployApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getDeployLogin();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import DeployApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = DeployApi(client).get_deploy_login()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.DeployAPI.GetDeployLogin(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, deploy_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = deploy_api::get_deploy_login(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.DeployApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new DeployApi(client).getDeployLogin();curl https://api.hanzo.ai/v1/deploy/login \
-H "Authorization: Bearer $HANZO_API_KEY"Tool deploy, op get_deploy_login — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "deploy",
"arguments": {
"op": "get_deploy_login",
"input": {}
}
}
}'Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/deploy/applications/{name}/resource-tree | Returns one application's argocd ApplicationTree: the objects the operator reconciled from its App CR, reached by ownerRef — the Deployment and,… |
GET /v1/deploy/applications/{name}/revisions/{revision}/metadata | Returns the argocd RevisionMetadata for one revision of one application — what the detail view shows beside a revision. |
POST /v1/deploy/applications/{name}/rollback | The console's rollback control — today it requests a reconcile, nothing more |
POST /v1/deploy/applications/{name}/sync | Ask the operator to reconcile one application now |
GET /v1/deploy/applications/{name}/syncwindows | Returns one application's argocd ApplicationSyncWindowState — the answer to "is anything blocking a sync of this application right now?". |
GET /v1/deploy/applications/{name} | Returns ONE projected argocd Application by name, with status.resources filled in from its reconciled resource tree — which is what makes it the… |
GET /v1/deploy/applications | Returns the fleet as an argocd ApplicationList: one projected Application per operator App CR, carrying the image tag the CR DECLARES, the tag… |
GET /v1/deploy/callback | Finish the sign-in round trip and mint the console session |
GET /v1/deploy/clusters | Returns the argocd ClusterList of the destinations the caller's applications reconcile into: one entry per distinct destination server, carrying the… |
GET /v1/deploy/gitops | Lists every Hanzo CD Application in the cluster: the git source each one polls, the commit it last APPLIED, how its last sync operation ended, and… |
GET /v1/deploy/health | Whether this control plane can actually reach the cluster it deploys to |
GET /v1/deploy/login | Start the sign-in round trip for this console |
POST /v1/deploy/logout | End the console session on this host |
GET /v1/deploy/projects | Returns the argocd AppProjectList this console groups and filters applications by. |
POST /v1/deploy/reconcile | Render the configured git source and apply it to the cluster, once |
GET /v1/deploy/session/userinfo | Answers "is this browser signed in, and if not where does it sign in?" — the dashboard SPA's bootstrap question, and the only route on this plane… |
GET /v1/deploy/settings | Returns the argocd AuthSettings object the dashboard SPA awaits before its first render. |
GET /v1/deploy/stream/applications/{name}/resource-tree | Live resource tree for one application, as Server-Sent Events |
GET /v1/deploy/stream/applications | Live application fleet updates as Server-Sent Events |
GET /v1/deploy/version | Returns the argocd VersionMessage the dashboard SPA reads at bootstrap. |
How is this guide?