Hanzo

Visor

Package visor is the compute you rent from Hanzo: machines, GPUs and clusters — launch one, resize it, tear it down.

Package visor is the compute you rent from Hanzo: machines, GPUs and clusters — launch one, resize it, tear it down.

Base URLhttps://api.hanzo.ai
Operations34
AuthAuthorization: Bearer $HANZO_API_KEY

Specification

HIP-1172 · visor — Compute You Rent — Draft · read the specification →

visor is the compute you rent from Hanzo: machines, GPUs and clusters — launch one, resize it, tear it down. It is the tenant's own view of that compute on the public API, covering both what Hanzo provisions and what the customer attached, and it fabricates nothing: a GPU row is a real machine's accelerator, a cluster is real node pools, and a field the source does not carry is omitted rather than invented. It is implemented in hanzoai/cloud at apps/visor (HIP-0106).

Motivation

Renting a machine is the one operation where the price must be knowable before the money moves, so every launch here carries a quote at the same address as the launch. And a fleet is the one inventory a tenant needs answered in a single question rather than four, so the board unions every source the org has — the machines Hanzo runs for it, the boxes it dialled in, its clusters, its agent run targets at /v1/agents/targets — under "what compute do I have, and how hot is it?". Both are properties an addressed face can have and a scatter of provider calls cannot.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

§1 It owns no store

Every row visor answers with is read from something that owns it, and visor persists none of it.

Provisioned compute is the compute service's: machines, node pools, the DOKS lifecycle. Attached compute is the per-org registry of bring-your-own clusters (apps/fleet), whose kubeconfigs are sealed in the org's KMS (HIP-1134) under a per-org, per-project ref — one registry with two readers, visor and model serving (HIP-1140), and therefore neither one's store. Utilization is the shared compute time series (apps/samples), one table in the datastore with org as its only tenant key, bound and never interpolated.

A capability that keeps a private copy of any of the three has two answers to one question. visor keeps none.

§2 The boundary: the machine, not what runs on it

visor answers for rented hardware. Everything a customer puts on it belongs somewhere else, and the split is clean at every neighbour:

  • platform (HIP-1230) is the container plane: builds, releases, environments, domains, logs. "Give me a GPU box" is visor; "run this image" is platform. Neither provisions the other's noun.
  • sandboxes (HIP-1146) is the compute primitive for somebody else's code — a pod whose lifetime is a request, on capacity that already exists. visor never executes a customer's code; it hands back a machine and stops.
  • bot owns /v1/bots/runs, which is a bot doing work on a desktop, live: a session. /v1/visor/compute/bots is a machine of kind bot plus its agent binding — rented hardware bootstrapped with a runtime. Two nouns share a word and do not share an address.
  • agents (HIP-1210) owns the agent. visor owns the binding that says a given machine hosts one, because the binding is a property of the machine.

Inside visor, /v1/visor/clusters is the fleet's answer to "what clusters do I have", provisioned and attached together, while /v1/visor/k8s is the provider lifecycle that creates and destroys them. Both are visor's, which is exactly why they fold under one name rather than being called two capabilities.

§3 The addresses

visor answers under /v1/visor, the one root HIP-0139 §3 gives a capability (manifest/apps.go:309), and every noun of the compute plane is one segment down (apps/visor/visor.go:143-268). The six top-level roots those nouns used to sit at — /v1/machines, /v1/gpus, /v1/clusters, /v1/k8s, /v1/fleet and /v1/compute — were six lines of the misfiled ratchet (HIP-0139 §5.1), and all six closed by fold (HIP-0139 §7.1), never by split: there is one owner and §1 leaves no second store to split along. No alias was left behind, so an address that begins anywhere else is not visor's.

  • /v1/visor/machines — the org's machines, one machine, its agent binding, and every binding in the fleet at /v1/visor/machines/agents.
  • /v1/visor/gpus — the accelerators on those machines, and /v1/visor/gpus/alerts.
  • /v1/visor/clusters — the cluster list, attach and detach, and node-pool create, scale and delete.
  • /v1/visor/k8s — cluster list, one cluster with its pools and worker nodes, create, delete, and the fleet-wide worker nodes at /v1/visor/k8s/nodes.
  • /v1/visor/fleet — the board, the attached workers at /v1/visor/fleet/workers, the utilization series at /v1/visor/fleet/samples, and the GPU job queue at /v1/visor/fleet/jobs with a cancel.
  • /v1/visor/compute — the launch catalog at /v1/visor/compute/regions and /v1/visor/compute/sizes, and bot machines under /v1/visor/compute/bots.

Every route with a shape to state is typed. Five are declared with prose beside the route, and each names why it cannot be a value:

  1. POST /v1/visor/machines and POST /v1/visor/compute/bots/launch — the response shape is chosen by the request: dryRun answers 200 with a price quote, a real launch answers 201 with the created resource. A typed op declares one Out, so typing either would have to change one of the two bodies.
  2. GET /v1/visor/compute/regions and GET /v1/visor/compute/sizes — the body is the launch catalog exactly as the compute service states it. visor does not know that shape, and inventing one is the opposite of what a passthrough is for.
  3. POST /v1/visor/compute/bots/{id}/{action} — a verb dispatch, not a resource. The message action streams the bound agent's answer back untouched: the upstream body, its content type and its status. There is no Out, and declaring one would buffer the stream.

The two launches spend real money, so their prose MUST state where the quote is; a caller reading only the document has no other place to learn it.

The address visor serves is not the address it dials. The compute service keeps its own spelling one segment apart — /v1/machines, /v1/machines/launch, /v1/k8s/clusters, /v1/k8s/nodes and their siblings on visor.hanzo.svc — and those literals are the wire, so they did not move with the fold (apps/visor/client.go). In this package a cl.call or cl.op path is upstream's and a zip.Get(reg, …) path is ours; the two are read apart by which one holds them, never by resemblance.

§4 Tenancy

The tenant is principal.Org, minted from the validated IAM owner claim (HIP-0026). It is forwarded to the compute service as owner=<org>, beside the gateway's own identity headers, which the edge has already sanitized of any client copy. It is never read from a body: a launch always lands in the caller's own tenant, and the ownership field a request might carry is ignored, not honoured. Absent a validated principal the answer is 403.

A machine is addressed upstream as owner plus name, so another tenant's id is not reachable rather than refused — the surface is not an existence oracle. Reads are open to any validated member of the org. The two mutations on /v1/visor/k8s/clusters, create and delete, additionally require a platform SuperAdmin or an admin of the caller's own org (requireClusterAdmin, HIP-0118), because provisioning spends on the house account rather than the customer's.

§5 Money

The surface declares cloud.Metered (plugin/visor/main.go).

A launch is not metered here. It fronts the compute service's resell endpoint, which owns the balance gate and the per-hour meter; visor forwards the tenant and returns what came back, and a dryRun quote is that service's price verbatim. Putting a second meter on this plane would be a second number for one machine.

visor meters exactly one unit of its own: attaching a bring-your-own cluster, kind byo-cluster, priced from the shared compute fee (CLOUD_COMPUTE_FEE_CENTS, whose source is the price list, HIP-1222). The customer brings the compute and the management plane is what is charged. The debit lands through the org's resource meter under the provider label compute — the commerce attribution and spend-cap scope key — and is gated before the register and metered after it, on the same key, so the amount authorized and the amount charged cannot drift. A deployment that prices the attach at zero is ungated exactly as it is unbilled. Every other route is free.

§6 Events and observability

visor publishes nothing on the bus: no visor.* event reaches a customer's webhooks. Beyond the request span every route already gets, it writes no audit record and exports no metric of its own.

What it does emit is the tenant's own data, on request. POST /v1/visor/fleet/samples is how an attached worker self-reports utilization; the sample is validated against the closed source and unit vocabularies, stamped with the caller's org, and appended to the shared series off the request path, so a slow or absent warehouse never fails an ingest. GET /v1/visor/fleet/samples reads that series back and GET /v1/visor/fleet folds the latest sample onto each unit.

§7 A partial answer says it is partial

Several reads here fold two independent sources, and a fold that loses one MUST still answer with the source that replied. An outage in a provider a tenant does not use must not take away the box the tenant does own.

The fold therefore carries the failure rather than swallowing it: a degraded list names the source that did not answer and gives a terse, log-safe reason — never the upstream's body, which for an unknown path is a page of markup. The field is additive and omitted when everything answered, so a healthy response is unchanged and a consumer that wants to tell an outage from an empty estate can. Without it, "the provider is down" and "you own nothing" are the same three bytes on the wire.

§8 Stage

ga. The manifest row declares no stage, and absent is ga (HIP-0139 §8).

§9 Upstream

The capability derives from no upstream code: apps/visor embeds nothing and holds one HTTP client. The compute service it fronts is hanzoai/visor, Apache-2.0, which derives from Casbin's Casibase with that notice preserved in its LICENSE.

That client speaks two wires and every call site says which, because they cannot be told apart by looking. The enveloped wire answers HTTP 200 with {status, msg, data}, so a logical failure is status: "error" at 200 and a bare status check reads it as success. The typed wire answers the value: the status is the outcome, 204 for a void result and 404 for a miss, with no envelope. Converting a noun from one to the other is a wire break and lands with its caller in the same change.

Rationale

The alternative to one face over a compute service is the console and the CLI each calling that service directly. It costs a second copy of the tenant rule in every client, and the tenant rule is the whole security model here. The alternative to folding attached compute into the same lists is a second cluster surface for bring-your-own, which answers "what clusters do I have" twice and eventually differently.

Security Considerations

The wrong implementation hands one tenant another tenant's compute. Three facts prevent it and none of them takes an input the caller controls: the owner is the validated claim, so a launch cannot be addressed elsewhere; a machine is identified upstream as owner plus name, so another tenant's id resolves to not-found rather than to a refusal that confirms it exists; and the identity headers forwarded are the gateway's own, the client's having been dropped at the edge.

The attach is the sharpest edge, because a kubeconfig is both a credential for a cluster and an instruction to dial a host. It is validated before it is sealed, at the one check that discovery folds and hand-pasted attaches both pass through: an exec or auth-provider credential plugin is refused, because honouring one runs a binary in the serving process's environment, and the apiserver must be a routable https endpoint, because a private address is a request to dial inside the cluster. What is stored is sealed in the org's KMS, so the custody nodes hold ciphertext, and it is never echoed back.

The admin gate on cluster create and delete protects Hanzo's own money rather than the tenant's data, which is why it is the only mutation here that asks for more than membership.

Four surfaces

SurfaceReaches this capability asCoverage
RESTvisor at its own prefix34 operations
CLIno command reaches it yet — use HTTP or an SDK
SDKVisorApi in every published client29 of 34 — the clients are generated at their own release
MCPtool visor on https://api.hanzo.ai/v1/mcp34 operations, 29 under the document's own id — ask describe for the rest

Quickstart

export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys

Then the first call — a read that needs nothing but the key. GET /v1/visor/gpus, operation listGpus:

hanzo has no subcommand for this operation — the CLI serves only what cloud's live route table confirms. Use HTTP or an SDK.

Answers 200 with object — ok.

Endpoints

EndpointWhat it does
POST /v1/visor/clusters/{clusterId}/pools/{poolId}/scaleResizes a node pool to an absolute node count and returns the pool as Visor reports it after the change.
DELETE /v1/visor/clusters/{clusterId}/pools/{poolId}Removes a node pool from one of the caller org's clusters.
POST /v1/visor/clusters/{clusterId}/poolsAdds a node pool to one of the caller org's clusters and answers 201 with the created pool.
DELETE /v1/visor/clusters/{id}Removes a BYO cluster from the caller org's fleet.
GET /v1/visor/clustersReturns the caller org's clusters from both sources: the managed clusters projected from Visor's node pools, and the BYO clusters attached to the…
POST /v1/visor/clustersAttaches a BYO cluster to the caller's org — the kubeconfig is validated, KMS-sealed and added to the fleet — and answers 201 with the cluster as it…
POST /v1/visor/compute/bots/{id}/{action}Message a bot, or stop it, by naming the action in the path
GET /v1/visor/compute/bots/{id}Returns one of the caller org's bot machines with its agent binding.
DELETE /v1/visor/compute/bots/{id}Tears down both halves of a bot: it unbinds the agent (best-effort — a bot with no binding still deletes), then terminates the machine.
POST /v1/visor/compute/bots/launchLaunch a bot machine — an agent plus the machine that runs it — or price one
GET /v1/visor/compute/botsReturns the caller org's bot machines — the kind=bot machines — each joined with the agent binding that says which cloud Agent it runs.
GET /v1/visor/compute/regionsThe regions a machine or GPU can be launched into
GET /v1/visor/compute/sizesThe machine and GPU sizes that can be launched
POST /v1/visor/fleet/jobs/{id}/cancelCancels a queued or running render in the caller's org.
GET /v1/visor/fleet/jobsReturns the caller org's gpu-jobs render queue, each row tagged with the GPU it targets (empty = the shared any-GPU lane) and the node claiming it,…
GET /v1/visor/fleet/samplesReturns the caller org's utilization series, oldest first.
POST /v1/visor/fleet/samplesRecords a BYO worker's live GPU utilization into the SAME series the fleet board overlays.
GET /v1/visor/fleet/workersReturns the caller org's BYO machines — the ones that dialed in via hanzo link — with everything each host reported about itself.
GET /v1/visor/fleetReturns every compute unit the caller's org has, from every source, each carrying its latest utilization: agent run-targets, the BYO machines that…
GET /v1/visor/gpus/alertsIs an HONEST empty surface: Visor exposes no GPU alert inventory, so this returns [] rather than fabricating alerts.
GET /v1/visor/gpusReturns one row per physical accelerator the caller's org has, derived from its real GPU machines (the size slug says how many cards a node holds)…
GET /v1/visor/k8s/clusters/{id}Returns one cluster's detail: node pools + worker nodes.
DELETE /v1/visor/k8s/clusters/{id}Destroys a DOKS cluster by id and answers 204.
GET /v1/visor/k8s/clustersLists the org's DOKS clusters (Visor, house account) folded with the org's BYO clusters — ONE fleet cluster view under the unified k8s noun.
POST /v1/visor/k8s/clustersProvisions a DOKS cluster for the caller's org and answers 201.
GET /v1/visor/k8s/nodesReturns every DOKS worker node in the org's clusters as a machine — the SAME set the fleet folds in (managedMachines), exposed directly under the k8s…
GET /v1/visor/machines/{id}/agentReturns the agent binding of one of the caller org's machines, or 404 when the machine runs no bot runtime.
PUT /v1/visor/machines/{id}/agentBinds a cloud Agent to one of the caller org's machines: the machine is recorded as running that Agent's @hanzo/bot runtime.
DELETE /v1/visor/machines/{id}/agentDetaches the agent runtime from one of the caller org's machines.
GET /v1/visor/machines/{id}Returns one of the caller org's machines by its org-scoped name.
DELETE /v1/visor/machines/{id}Terminates one of the caller org's machines.
GET /v1/visor/machines/agentsReturns every agent↔machine binding in the caller's org — which machines are running which cloud Agent, with vm's own reconciled status.
GET /v1/visor/machinesReturns every machine the caller's org has — Visor's registry, the live DigitalOcean droplets and the DOKS worker nodes (deduped into one union),…
POST /v1/visor/machinesLaunch a metered machine for your org, or price one first with dryRun

All Hanzo APIs · Interactive reference

How is this guide?

On this page