{ "openapi": "3.1.0", "info": { "title": "Hanzo Cloud API", "description": "Package visor is the compute you rent from Hanzo: machines, GPUs and clusters — launch one, resize it, tear it down.", "version": "v1" }, "servers": [ { "url": "https://api.hanzo.ai" } ], "tags": [ { "name": "clusters" }, { "name": "compute" }, { "name": "fleet" }, { "name": "gpus" }, { "name": "k8s" }, { "name": "machines" } ], "paths": { "/v1/clusters": { "get": { "operationId": "listClusters", "summary": "Returns the caller org's clusters from both sources: the managed clusters projected from Visor's node pools, and the BYO clusters attached to the caller's project.", "description": "Returns the caller org's clusters from both sources: the managed\nclusters projected from Visor's node pools, and the BYO clusters attached to the\ncaller's project. A Visor outage costs the managed half only — the BYO half\nstill lists, because a page that 502s on an optional provider is worse than a\npage that shows what it can.", "tags": [ "clusters" ], "responses": { "200": { "content": { "application/json": { "example": { "clusters": [ { "doksClusterId": "cl-1", "kind": "managed", "name": "prod", "nodeCount": 2, "nodePools": [ { "count": 2, "name": "gpu", "poolId": "p-1", "size": "gpu-h100x8-640gb" } ], "nodeSize": "gpu-h100x8-640gb", "status": "running" } ] }, "schema": { "$ref": "#/components/schemas/clusterList" } } }, "description": "ok" } } }, "post": { "operationId": "attachCluster", "summary": "Attaches 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 now appears on GET /v1/clusters.", "description": "Attaches a BYO cluster to the caller's org — the kubeconfig is\nvalidated, KMS-sealed and added to the fleet — and answers 201 with the cluster\nas it now appears on GET /v1/clusters. Billed the nominal management fee: the\ncustomer brings the compute, Hanzo meters the management plane.", "tags": [ "clusters" ], "requestBody": { "content": { "application/json": { "example": { "default": false, "kubeconfig": "apiVersion: v1\nkind: Config\n...", "name": "lab", "provider": "on-prem" }, "schema": { "$ref": "#/components/schemas/clusterAttach" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "example": { "kind": "byo", "name": "lab", "nodeCount": 3, "nodePools": [], "nvidiaGpu": 2, "region": "on-prem", "status": "attached" }, "schema": { "$ref": "#/components/schemas/clusterView" } } }, "description": "ok" } } } }, "/v1/clusters/{clusterId}/pools": { "post": { "operationId": "createNodePool", "summary": "Adds a node pool to one of the caller org's clusters and answers 201 with the created pool.", "description": "Adds a node pool to one of the caller org's clusters and answers 201\nwith the created pool. Only the CreateNodePoolSpec fields are forwarded;\nowner/provider/clusterId ride in the query exactly as Visor expects them.", "tags": [ "clusters" ], "parameters": [ { "name": "clusterId", "in": "path", "required": true, "description": "ClusterID is the cluster to add the pool to, from the URL path.", "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "example": { "autoScale": false, "count": 2, "name": "gpu", "provider": "digitalocean", "size": "gpu-h100x8-640gb" }, "schema": { "$ref": "#/components/schemas/poolCreate" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "example": { "count": 2, "name": "gpu", "poolId": "p-1", "size": "gpu-h100x8-640gb" }, "schema": { "$ref": "#/components/schemas/nodePoolView" } } }, "description": "ok" } } } }, "/v1/clusters/{clusterId}/pools/{poolId}": { "delete": { "operationId": "deleteNodePool", "summary": "Removes a node pool from one of the caller org's clusters.", "description": "Removes a node pool from one of the caller org's clusters. The owner\nscopes the delete to the caller's tenant; provider+clusterId drive the\nprovider-side removal. Answers 204.", "tags": [ "clusters" ], "parameters": [ { "name": "clusterId", "in": "path", "required": true, "description": "ClusterID and PoolID address the pool, from the URL path.", "schema": { "type": "string" } }, { "name": "poolId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "provider", "in": "query", "required": false, "description": "Provider is the cloud the cluster lives on, from ?provider=. Required.", "schema": { "type": "string" } } ], "responses": { "204": { "description": "no content" } } } }, "/v1/clusters/{clusterId}/pools/{poolId}/scale": { "post": { "operationId": "scaleNodePool", "summary": "Resizes a node pool to an absolute node count and returns the pool as Visor reports it after the change.", "description": "Resizes a node pool to an absolute node count and returns the pool as\nVisor reports it after the change.", "tags": [ "clusters" ], "parameters": [ { "name": "clusterId", "in": "path", "required": true, "description": "ClusterID and PoolID address the pool, from the URL path.", "schema": { "type": "string" } }, { "name": "poolId", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "example": { "count": 4, "provider": "digitalocean" }, "schema": { "$ref": "#/components/schemas/poolScale" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "example": { "count": 4, "name": "gpu", "poolId": "p-1", "size": "gpu-h100x8-640gb" }, "schema": { "$ref": "#/components/schemas/nodePoolView" } } }, "description": "ok" } } } }, "/v1/clusters/{id}": { "delete": { "operationId": "detachCluster", "summary": "Removes a BYO cluster from the caller org's fleet.", "description": "Removes a BYO cluster from the caller org's fleet. It only ever\ntouches BYO clusters — a managed cluster's nodes are removed through the node-pool\nroutes — and answers 404 when the name is not in this org's fleet.", "tags": [ "clusters" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the cluster's fleet name (the `name` it was attached under), matched\nlower-cased.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "example": { "detached": "lab" }, "schema": { "$ref": "#/components/schemas/clusterDetached" } } }, "description": "ok" } } } }, "/v1/compute/bots": { "get": { "operationId": "listBots", "summary": "Returns the caller org's bot machines — the kind=bot machines — each joined with the agent binding that says which cloud Agent it runs.", "description": "Returns the caller org's bot machines — the kind=bot machines — each\njoined with the agent binding that says which cloud Agent it runs.\n\nThe bindings are read ONCE and joined by machine id, so the list is O(1) upstream\ncalls, not N+1. A bindings read that fails only costs the reconciled status: a bot\nstill lists without it.", "tags": [ "compute" ], "responses": { "200": { "content": { "application/json": { "example": { "bots": [ { "agent": "bot-a", "binding": { "agentName": "bot-a", "machineId": "drop-a", "status": "running" }, "id": "drop-a", "name": "bot-a", "status": "running" } ] }, "schema": { "$ref": "#/components/schemas/botList" } } }, "description": "ok" } } } }, "/v1/compute/bots/launch": { "post": { "operationId": "post_v1_compute_bots_launch", "summary": "Launch a bot machine — an agent plus the machine that runs it — or price one", "description": "Creates BOTH halves of a bot in one call and answers 201 with the bot: the cloud agent it runs, then a bot-kind machine bootstrapped with the bot runtime, then the binding between them, so a launched bot is immediately messageable. Send `dryRun: true` for a price quote instead — 200 with the upstream quote verbatim, no agent created, no machine launched, nothing spent.\n\nThe agent is created FIRST and on purpose: it is create-if-absent (an agent that already exists is reused, so a relaunch is fine and several bots may share one explicit `agent`), and doing it before the machine means a bad request — a model that is not in the catalog, say — fails with the real reason BEFORE any metered machine is provisioned. `agent` defaults to the bot's name and an empty `model` takes the deployment default.\n\nOrg-scoped and fails closed: a validated principal is required (403 without one), the owning org is that principal's and never a body field, `size` is required (400), and `name` is required for a real launch though not for a quote.", "tags": [ "compute" ] } }, "/v1/compute/bots/{id}": { "delete": { "operationId": "deleteBot", "summary": "Tears down both halves of a bot: it unbinds the agent (best-effort — a bot with no binding still deletes), then terminates the machine.", "description": "Tears down both halves of a bot: it unbinds the agent (best-effort — a\nbot with no binding still deletes), then terminates the machine. Answers 204.", "tags": [ "compute" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the bot machine's id — the same id the machines surface addresses it\nby. Scoped to the caller's org upstream, so another tenant's id is 404.", "schema": { "type": "string" } } ], "responses": { "204": { "description": "no content" } } }, "get": { "operationId": "getBot", "summary": "Returns one of the caller org's bot machines with its agent binding.", "description": "Returns one of the caller org's bot machines with its agent binding.\n\nA machine counts as a Bot if it carries the hanzo-kind:bot tag OR has an agent\nbinding — either signal is authoritative, so a bot resolves even before its\ncloud-init has stamped every tag. A machine that is neither is 404: this route\nanswers for bots, not for machines.", "tags": [ "compute" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the bot machine's id — the same id the machines surface addresses it\nby. Scoped to the caller's org upstream, so another tenant's id is 404.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "example": { "agent": "bot-a", "binding": { "agentName": "bot-a", "machineId": "drop-a", "status": "running" }, "id": "drop-a", "name": "bot-a", "status": "running" }, "schema": { "$ref": "#/components/schemas/botView" } } }, "description": "ok" } } } }, "/v1/compute/bots/{id}/{action}": { "post": { "operationId": "post_v1_compute_bots_by_id_by_action", "summary": "Message a bot, or stop it, by naming the action in the path", "description": "Dispatches one verb against a bot the caller's org owns. `message` runs the bot's bound agent with the request body as the message and streams the agent's answer back VERBATIM — the upstream body, its content type and its status — so a message is a real agent run, recorded, billed and traced exactly like any other, under the caller's own identity rather than a fabricated one. `stop` and `pause` are the same single honest capability: they halt the runtime by unbinding the agent while LEAVING THE MACHINE UP, so the bot stops answering but keeps costing — rebind to resume, or delete the bot to tear it down. Stopping is idempotent; a bot with no binding still reports stopped.\n\nOrg-scoped and fails closed: a validated principal is required (403 without one) and the bot is addressed under the caller's OWN org, so another tenant's id is not reachable. An unknown action is a clean 400 naming the three it accepts, never a silent no-op, and messaging a bot with no bound agent is a 400.", "tags": [ "compute" ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "action", "in": "path", "required": true, "schema": { "type": "string" } } ] } }, "/v1/compute/regions": { "get": { "operationId": "get_v1_compute_regions", "summary": "The regions a machine or GPU can be launched into", "description": "Lists the launch regions the compute catalog offers, passed through verbatim from the provider so the shape stays the provider's single source of truth. The catalog is GLOBAL, not per-tenant: no owner is forwarded and every org sees the same list. It is still gated — a validated principal is required, 403 without one — because the catalog is what backs the launch drawer, not public marketing copy.", "tags": [ "compute" ] } }, "/v1/compute/sizes": { "get": { "operationId": "get_v1_compute_sizes", "summary": "The machine and GPU sizes that can be launched", "description": "Lists the instance sizes the compute catalog offers, passed through verbatim from the provider so the shape stays the provider's single source of truth. These are the values `size` accepts on a launch. The catalog is GLOBAL, not per-tenant: no owner is forwarded and every org sees the same list. It is still gated — a validated principal is required, 403 without one.", "tags": [ "compute" ] } }, "/v1/fleet": { "get": { "operationId": "listFleet", "summary": "Returns every compute unit the caller's org has, from every source, each carrying its latest utilization: agent run-targets, the BYO machines that dialed in, attached BYO clusters and Visor-provisioned machines.", "description": "Returns every compute unit the caller's org has, from every source, each\ncarrying its latest utilization: agent run-targets, the BYO machines that dialed\nin, attached BYO clusters and Visor-provisioned machines.\n\nA unit with a live snapshot of its own keeps it; the rest are overlaid from the\nutilization series, and only when the sample agrees about the SOURCE — two planes\ncould mint the same unit id, and a board must never show one machine's load on\nanother's row. BYO GPU units also carry their gpu-jobs queue depth. Every source\nis folded in independently: a broken one costs its own rows and nothing else.", "tags": [ "fleet" ], "responses": { "200": { "content": { "application/json": { "example": { "units": [ { "host": "spark", "kind": "worker", "label": "spark", "metrics": { "at": "2026-07-27T09:00:00Z", "gpuUtil": 0.42 }, "queued": 2, "running": 1, "sessions": 0, "source": "byo", "spec": { "arch": "arm64", "cpus": 20, "gpuModel": "NVIDIA GB10", "gpus": 1, "os": "linux" }, "status": "online", "unit": "spark" } ] }, "schema": { "$ref": "#/components/schemas/fleetBoard" } } }, "description": "ok" } } } }, "/v1/fleet/jobs": { "get": { "operationId": "listFleetJobs", "summary": "Returns 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, optionally narrowed to one GPU's queue and/or one status.", "description": "Returns the caller org's gpu-jobs render queue, each row tagged with\nthe GPU it targets (empty = the shared any-GPU lane) and the node claiming it,\noptionally narrowed to one GPU's queue and/or one status.\n\nA job whose worker died — STARTED with an elapsed lease and not yet reclaimed —\nreads \"stalled\", not \"running\". Fail-soft: an unavailable tasks engine yields an\nempty queue rather than an error.", "tags": [ "fleet" ], "parameters": [ { "name": "gpu", "in": "query", "required": false, "description": "GPU selects one node's lane: jobs TARGETED at it (gpu:\u003cnode\u003e) or CLAIMED by\nit. The literal \"shared\" selects the any-GPU lane — no target, no claimant.\nMatched case-insensitively.", "schema": { "type": "string" } }, { "name": "status", "in": "query", "required": false, "description": "Status selects one lifecycle state: queued, running, stalled, completed,\nfailed or canceled.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "example": { "jobs": [ { "attempt": 1, "gpu": "spark", "id": "job-1", "label": "hero", "runId": "job-1", "status": "running", "type": "studio.render", "worker": "spark" } ] }, "schema": { "$ref": "#/components/schemas/jobList" } } }, "description": "ok" } } } }, "/v1/fleet/jobs/{id}/cancel": { "post": { "operationId": "cancelFleetJob", "summary": "Cancels a queued or running render in the caller's org.", "description": "Cancels a queued or running render in the caller's org. The engine\ncancel is org-scoped, so a tenant can only ever cancel its OWN job: a job in\nanother tenant's shard is 404, exactly like one that never existed. An\nalready-finished job is 409.", "tags": [ "fleet" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the job (activity) id, from the URL path.", "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "example": { "reason": "superseded" }, "schema": { "$ref": "#/components/schemas/jobCancel" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "example": { "canceled": "job-1", "run": "job-1" }, "schema": { "$ref": "#/components/schemas/jobCanceled" } } }, "description": "ok" } } } }, "/v1/fleet/samples": { "get": { "operationId": "listFleetSamples", "summary": "Returns the caller org's utilization series, oldest first.", "description": "Returns the caller org's utilization series, oldest first.\n\nA rejected narrower is a 400 carrying its own reason (the vocabulary is ours and\nsafe to echo); a warehouse failure is logged and answered 503 \"unavailable\",\nbecause a chart that silently reads \"no load\" when the truth is \"we cannot tell\"\nis worse than one that says so. An ABSENT warehouse is different again: it returns\nan empty series, which renders honestly as \"no samples yet\".", "tags": [ "fleet" ], "parameters": [ { "name": "unit", "in": "query", "required": false, "description": "Unit selects one compute unit's series by its source-local id.", "schema": { "type": "string" } }, { "name": "source", "in": "query", "required": false, "description": "Source selects one plane: \"agent\", \"byo\" or \"visor\".", "schema": { "type": "string" } }, { "name": "range", "in": "query", "required": false, "description": "Range is the lookback window (e.g. \"1h\", \"24h\", \"7d\"); empty takes the\nwarehouse default.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "example": { "samples": [ { "at": "2026-07-27T09:00:00Z", "gpuModel": "GB10", "gpuUtil": 0.42, "gpus": 1, "host": "spark", "kind": "worker", "source": "byo", "unit": "spark" } ] }, "schema": { "$ref": "#/components/schemas/sampleList" } } }, "description": "ok" } } }, "post": { "operationId": "recordFleetSample", "summary": "Records a BYO worker's live GPU utilization into the SAME series the fleet board overlays.", "description": "Records a BYO worker's live GPU utilization into the SAME series the\nfleet board overlays. The org is the validated principal and source/kind are fixed\nserver-side, so a worker names only its own metrics — never another tenant or\nanother source. Answers 202: the warehouse write is DETACHED (its own bounded\ncontext, never in the response path), so a slow or absent warehouse cannot stall a\nheartbeat.", "tags": [ "fleet" ], "requestBody": { "content": { "application/json": { "example": { "gpuModel": "GB10", "gpuUtil": 0.42, "gpus": 1, "host": "spark", "memFree": 200, "memUsed": 100, "unit": "spark" }, "schema": { "$ref": "#/components/schemas/sampleIngest" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "example": { "recorded": true }, "schema": { "$ref": "#/components/schemas/sampleAccepted" } } }, "description": "ok" } } } }, "/v1/fleet/workers": { "get": { "operationId": "listFleetWorkers", "summary": "Returns the caller org's BYO machines — the ones that dialed in via `hanzo link` — with everything each host reported about itself.", "description": "Returns the caller org's BYO machines — the ones that dialed in\nvia `hanzo link` — with everything each host reported about itself. The Machines\nand GPUs pages fold the same data into their normalized shapes; this is the\ncanonical raw list a fleet view (or the CLI's `status`) reads.", "tags": [ "fleet" ], "responses": { "200": { "content": { "application/json": { "example": { "workers": [ { "arch": "arm64", "cpus": 20, "gpus": [ { "memoryTotal": "122880 MiB", "name": "NVIDIA GB10" } ], "hostname": "spark", "id": "spark", "location": "on-prem", "provider": "byo", "status": "online" } ] }, "schema": { "$ref": "#/components/schemas/workerList" } } }, "description": "ok" } } } }, "/v1/gpus": { "get": { "operationId": "listGpus", "summary": "Returns 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) and from the accelerators BYO workers report through nvidia-smi.", "description": "Returns one row per physical accelerator the caller's org has, derived\nfrom its real GPU machines (the size slug says how many cards a node holds) and\nfrom the accelerators BYO workers report through nvidia-smi.\n\nLive telemetry is absent on Visor rows because Visor's machine object carries\nnone — an honest omission the console renders as \"—\", never a fabricated 0.", "tags": [ "gpus" ], "responses": { "200": { "content": { "application/json": { "example": { "gpus": [ { "id": "gpu-1#0", "machine": "gpu-1", "model": "H100", "name": "gpu-1", "provider": "digitalocean", "region": "nyc2", "status": "running" } ] }, "schema": { "$ref": "#/components/schemas/gpuList" } } }, "description": "ok" } } } }, "/v1/gpus/alerts": { "get": { "operationId": "listGpuAlerts", "summary": "Is an HONEST empty surface: Visor exposes no GPU alert inventory, so this returns [] rather than fabricating alerts.", "description": "Is an HONEST empty surface: Visor exposes no GPU alert inventory, so\nthis returns [] rather than fabricating alerts. It stays a real, tenant-gated\nroute so the console's alerts fetch resolves (200 [], not a 404) — an honest\n\"no alerts\", the same discipline the rest of the surface follows.", "tags": [ "gpus" ], "responses": { "200": { "content": { "application/json": { "example": { "alerts": [] }, "schema": { "$ref": "#/components/schemas/gpuAlertList" } } }, "description": "ok" } } } }, "/v1/k8s/clusters": { "get": { "operationId": "listKubernetesClusters", "summary": "Lists the org's DOKS clusters (Visor, house account) folded with the org's BYO clusters — ONE fleet cluster view under the unified k8s noun.", "description": "Lists the org's DOKS clusters (Visor, house account) folded with\nthe org's BYO clusters — ONE fleet cluster view under the unified k8s noun. A Visor\noutage is logged and skipped so a down optional provider never hides the BYO list.", "tags": [ "k8s" ], "responses": { "200": { "content": { "application/json": { "example": { "clusters": [ { "doClusterId": "cl-1", "doksClusterId": "cl-1", "kind": "managed", "name": "prod", "nodeCount": 0, "nodePools": [], "region": "nyc3", "status": "running" } ] }, "schema": { "$ref": "#/components/schemas/clusterList" } } }, "description": "ok" } } }, "post": { "operationId": "createKubernetesCluster", "summary": "Provisions a DOKS cluster for the caller's org and answers 201.", "description": "Provisions a DOKS cluster for the caller's org and answers 201.\nADMIN-GATED — a SuperAdmin, or an OrgAdmin of the caller's own org — because\nprovisioning spends real infrastructure on the house account. The request is\nvalidated at this boundary, then Visor owns provisioning and the hanzo-org\nownership tag.", "tags": [ "k8s" ], "requestBody": { "content": { "application/json": { "example": { "name": "prod", "nodePool": { "count": 2, "name": "gpu", "size": "gpu-h100x8-640gb" }, "region": "nyc3" }, "schema": { "$ref": "#/components/schemas/createClusterReq" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "example": { "doClusterId": "cl-1", "doksClusterId": "cl-1", "kind": "managed", "name": "prod", "nodeCount": 0, "nodePools": [], "region": "nyc3", "status": "provisioning" }, "schema": { "$ref": "#/components/schemas/clusterView" } } }, "description": "ok" } } } }, "/v1/k8s/clusters/{id}": { "delete": { "operationId": "deleteKubernetesCluster", "summary": "Destroys a DOKS cluster by id and answers 204.", "description": "Destroys a DOKS cluster by id and answers 204. ADMIN-GATED, like\ncreate. Visor scopes the delete to the org (refuses a foreign id), so this can\nonly ever remove the caller org's own cluster.", "tags": [ "k8s" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the provider's DOKS cluster id. Visor scopes the lookup to the caller's\norg, so another tenant's id resolves to not-found rather than their cluster.", "schema": { "type": "string" } } ], "responses": { "204": { "description": "no content" } } }, "get": { "operationId": "getKubernetesCluster", "summary": "Returns one cluster's detail: node pools + worker nodes.", "description": "Returns one cluster's detail: node pools + worker nodes. Visor scopes\nthe lookup to the org (a foreign or missing id resolves to not-found), so a tenant\ncan never read another tenant's cluster by guessing an id.", "tags": [ "k8s" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the provider's DOKS cluster id. Visor scopes the lookup to the caller's\norg, so another tenant's id resolves to not-found rather than their cluster.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "example": { "doksClusterId": "cl-1", "kind": "managed", "name": "prod", "nodeCount": 1, "nodePools": [ { "count": 1, "name": "gpu", "poolId": "p-1", "size": "gpu-h100x8-640gb" } ], "nodeSize": "gpu-h100x8-640gb", "nodes": [ { "id": "node-1", "name": "node-1", "status": "active" } ], "region": "nyc3", "status": "running" }, "schema": { "$ref": "#/components/schemas/clusterDetailView" } } }, "description": "ok" } } } }, "/v1/k8s/nodes": { "get": { "operationId": "listKubernetesNodes", "summary": "Returns 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 noun.", "description": "Returns every DOKS worker node in the org's clusters as a machine —\nthe SAME set the fleet folds in (managedMachines), exposed directly under the k8s\nnoun. House account (hanzo-org cluster tag) + BYOC, deduped by Visor.", "tags": [ "k8s" ], "responses": { "200": { "content": { "application/json": { "example": { "nodes": [ { "id": "node-1", "name": "node-1", "region": "nyc3", "status": "active", "type": "s-4vcpu-8gb", "vcpu": 4 } ] }, "schema": { "$ref": "#/components/schemas/nodeList" } } }, "description": "ok" } } } }, "/v1/machines": { "get": { "operationId": "listMachines", "summary": "Returns every machine the caller's org has — Visor's registry, the live DigitalOcean droplets and the DOKS worker nodes (deduped into one union), plus the BYO machines that dialed in via `hanzo link` (provider \"byo\").", "description": "Returns every machine the caller's org has — Visor's registry, the\nlive DigitalOcean droplets and the DOKS worker nodes (deduped into one union),\nplus the BYO machines that dialed in via `hanzo link` (provider \"byo\").\n\nA source Visor cannot answer for is logged and skipped, never an error: one\nwedged upstream must not hide the machines the other sources can see.", "tags": [ "machines" ], "responses": { "200": { "content": { "application/json": { "example": { "machines": [ { "id": "web-1", "mem": "4 GB", "name": "Web 1", "provider": "digitalocean", "publicIp": "1.2.3.4", "region": "sfo3", "status": "running", "type": "s-2vcpu-4gb", "vcpu": 2 } ] }, "schema": { "$ref": "#/components/schemas/machineList" } } }, "description": "ok" } } }, "post": { "operationId": "post_v1_machines", "summary": "Launch a metered machine for your org, or price one first with dryRun", "description": "Provisions a machine owned by the caller's org and answers 201 with the machine. Send `dryRun: true` to get a PRICE QUOTE instead: 200 with the upstream quote passed through verbatim, nothing launched and nothing spent. Two response shapes on one address is the rule to know, and it is why this is not a typed op.\n\nMetering is not this plane's: the launch fronts the compute provider's resell endpoint, which owns the balance gate and the per-hour meter, and cloud only forwards the tenant. Ownership is the validated principal's org and is never read from the body, so a launch always lands in the caller's OWN tenant and the machine it creates is only ever visible to that tenant. Fails closed: a validated principal is required (403 without one) and `size` (or its `instanceType` alias) is required (400).", "tags": [ "machines" ] } }, "/v1/machines/agents": { "get": { "operationId": "listMachineAgents", "summary": "Returns every agent↔machine binding in the caller's org — which machines are running which cloud Agent, with vm's own reconciled status.", "description": "Returns every agent↔machine binding in the caller's org — which\nmachines are running which cloud Agent, with vm's own reconciled status.", "tags": [ "machines" ], "responses": { "200": { "content": { "application/json": { "example": { "agentBindings": [ { "agentName": "bot-a", "machineId": "drop-a", "publicIp": "1.2.3.4", "status": "running" } ] }, "schema": { "$ref": "#/components/schemas/bindingList" } } }, "description": "ok" } } } }, "/v1/machines/{id}": { "delete": { "operationId": "deleteMachine", "summary": "Terminates one of the caller org's machines.", "description": "Terminates one of the caller org's machines. Visor takes the\nmachine identity as owner+name, and the owner is the validated principal, so a\ncaller can only ever terminate its own tenant's machine. Answers 204.", "tags": [ "machines" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the machine's org-scoped NAME — the stable key Visor addresses a\nmachine by (owner/name), not the ephemeral provider id.", "schema": { "type": "string" } } ], "responses": { "204": { "description": "no content" } } }, "get": { "operationId": "getMachine", "summary": "Returns one of the caller org's machines by its org-scoped name.", "description": "Returns one of the caller org's machines by its org-scoped name.\nVisor keys the lookup by owner/name, so an id belonging to another tenant\nresolves to not-found rather than another org's machine.", "tags": [ "machines" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the machine's org-scoped NAME — the stable key Visor addresses a\nmachine by (owner/name), not the ephemeral provider id.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "example": { "id": "web-1", "name": "Web 1", "publicIp": "1.2.3.4", "region": "sfo3", "status": "running", "type": "s-2vcpu-4gb", "vcpu": 2 }, "schema": { "$ref": "#/components/schemas/machineView" } } }, "description": "ok" } } } }, "/v1/machines/{id}/agent": { "delete": { "operationId": "unbindMachineAgent", "summary": "Detaches the agent runtime from one of the caller org's machines.", "description": "Detaches the agent runtime from one of the caller org's\nmachines. The machine stays — this halts the bot, it does not terminate the\ncompute. Answers 204.", "tags": [ "machines" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the machine's org-scoped NAME — the stable key Visor addresses a\nmachine by (owner/name), not the ephemeral provider id.", "schema": { "type": "string" } } ], "responses": { "204": { "description": "no content" } } }, "get": { "operationId": "getMachineAgent", "summary": "Returns the agent binding of one of the caller org's machines, or 404 when the machine runs no bot runtime.", "description": "Returns the agent binding of one of the caller org's\nmachines, or 404 when the machine runs no bot runtime.", "tags": [ "machines" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the machine's org-scoped NAME — the stable key Visor addresses a\nmachine by (owner/name), not the ephemeral provider id.", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "example": { "agentName": "bot-a", "botVersion": "1.4.0", "machineId": "drop-a", "status": "running" }, "schema": { "$ref": "#/components/schemas/agentBinding" } } }, "description": "ok" } } }, "put": { "operationId": "bindMachineAgent", "summary": "Binds a cloud Agent to one of the caller org's machines: the machine is recorded as running that Agent's @hanzo/bot runtime.", "description": "Binds a cloud Agent to one of the caller org's machines: the\nmachine is recorded as running that Agent's @hanzo/bot runtime. The owning org is\nthe validated tenant, never a client field.", "tags": [ "machines" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "ID is the machine to bind, from the URL path.", "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "example": { "agentName": "bot-a", "botVersion": "1.4.0" }, "schema": { "$ref": "#/components/schemas/bindAgentReq" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "example": { "agentName": "bot-a", "machineId": "drop-a", "status": "binding" }, "schema": { "$ref": "#/components/schemas/agentBinding" } } }, "description": "ok" } } } } }, "components": { "schemas": { "agentBinding": { "properties": { "agentName": { "type": "string" }, "botVersion": { "type": "string" }, "createdTime": { "type": "string" }, "machineId": { "type": "string" }, "message": { "type": "string" }, "name": { "type": "string" }, "org": { "type": "string" }, "owner": { "type": "string" }, "provider": { "type": "string" }, "publicIp": { "type": "string" }, "status": { "type": "string" }, "updatedTime": { "type": "string" } }, "type": "object" }, "bindAgentReq": { "properties": { "agentName": { "description": "AgentName is the cloud Agent (/v1/agents) the machine will run. Required.", "type": "string" }, "botVersion": { "description": "BotVersion pins the @hanzo/bot runtime version; empty takes the default.", "type": "string" }, "id": { "description": "ID is the machine to bind, from the URL path.", "type": "string" } }, "type": "object" }, "bindingList": { "properties": { "agentBindings": { "description": "AgentBindings is one row per bound machine, emitted verbatim as vm reports\nit.", "items": { "$ref": "#/components/schemas/agentBinding" }, "type": "array" } }, "type": "object" }, "botList": { "properties": { "bots": { "description": "Bots is one row per kind=bot machine, each joined with its agent binding\nwhen it has one.", "items": { "$ref": "#/components/schemas/botView" }, "type": "array" } }, "type": "object" }, "botView": { "properties": { "agent": { "type": "string" }, "binding": { "$ref": "#/components/schemas/agentBinding" }, "createdTime": { "type": "string" }, "gpu": { "type": "string" }, "id": { "type": "string" }, "image": { "type": "string" }, "mem": { "type": "string" }, "name": { "type": "string" }, "os": { "type": "string" }, "privateIp": { "type": "string" }, "provider": { "type": "string" }, "publicIp": { "type": "string" }, "region": { "type": "string" }, "status": { "type": "string" }, "type": { "type": "string" }, "vcpu": { "type": "integer" } }, "type": "object" }, "byoGPU": { "properties": { "arch": { "description": "native target, e.g. \"gfx1151\"", "type": "string" }, "memoryTotal": { "description": "VRAM (or unified pool), e.g. \"122880 MiB\"", "type": "string" }, "name": { "type": "string" }, "unified": { "description": "unified CPU/GPU memory pool (APU / SoC)", "type": "boolean" } }, "type": "object" }, "byoWorker": { "properties": { "arch": { "description": "Arch/CPUs/Memory are the connecting host's static CPU spec, mirrored from the\nregistration: Arch is runtime.GOARCH (amd64 | arm64), Memory is total RAM in\nBYTES — the same fields a code-linked run-target carries, so the /v1/fleet\nboard renders a linked node's arch + cores + RAM like any other unit.", "type": "string" }, "capabilities": { "description": "Capabilities the worker advertises (\"studio.render\", \"engine.serve\"); Engine\nis present when it runs a hanzo-engine model server. Both additive + omitempty.", "items": { "type": "string" }, "type": "array" }, "cpuModel": { "type": "string" }, "cpus": { "type": "integer" }, "cuda": { "type": "string" }, "driver": { "type": "string" }, "engine": { "$ref": "#/components/schemas/engineAdvertisement" }, "firstSeen": { "type": "string" }, "gpus": { "items": { "$ref": "#/components/schemas/byoGPU" }, "type": "array" }, "hip": { "type": "string" }, "hostname": { "type": "string" }, "id": { "type": "string" }, "jobQueue": { "type": "string" }, "lastHeartbeat": { "type": "string" }, "location": { "description": "\"on-prem\" (BYO has no cloud region)", "type": "string" }, "memory": { "type": "integer" }, "os": { "type": "string" }, "provider": { "description": "always \"byo\"", "type": "string" }, "rocm": { "type": "string" }, "status": { "description": "online | offline", "type": "string" }, "version": { "type": "string" } }, "type": "object" }, "clusterAttach": { "properties": { "default": { "description": "Default marks this the org's default cluster for scheduling.", "type": "boolean" }, "kubeconfig": { "description": "Kubeconfig is the cluster's kubeconfig, verbatim. Required — a body without\none is not an attach.", "type": "string" }, "name": { "description": "Name is the fleet-local name for the cluster; lower-cased, and the key the\ndetach route addresses it by. Required.", "type": "string" }, "provider": { "description": "Provider is a free-form label for where the cluster runs (\"gke\", \"on-prem\");\nit is display only, not a routing key.", "type": "string" } }, "type": "object" }, "clusterDetached": { "properties": { "detached": { "description": "Detached is the lower-cased fleet name that was removed.", "type": "string" } }, "type": "object" }, "clusterDetailView": { "properties": { "amdGpu": { "type": "integer" }, "createdAt": { "type": "string" }, "doClusterId": { "type": "string" }, "doksClusterId": { "type": "string" }, "kind": { "type": "string" }, "name": { "type": "string" }, "nodeCount": { "type": "integer" }, "nodePools": { "items": { "$ref": "#/components/schemas/nodePoolView" }, "type": "array" }, "nodeSize": { "type": "string" }, "nodes": { "items": { "$ref": "#/components/schemas/machineView" }, "type": "array" }, "nvidiaGpu": { "type": "integer" }, "region": { "type": "string" }, "status": { "type": "string" } }, "type": "object" }, "clusterList": { "properties": { "clusters": { "description": "Clusters is the merged fleet — kind \"managed\" for Visor-provisioned, \"byo\"\nfor an attached kubeconfig.", "items": { "$ref": "#/components/schemas/clusterView" }, "type": "array" }, "degraded": { "description": "Degraded names any source that did not answer, so an empty Clusters means\n\"you have none\" only when this is absent. Omitted when everything answered,\nso a healthy response is unchanged. See degraded.go.", "items": { "$ref": "#/components/schemas/sourceFailure" }, "type": "array" } }, "type": "object" }, "clusterView": { "properties": { "amdGpu": { "type": "integer" }, "createdAt": { "type": "string" }, "doClusterId": { "type": "string" }, "doksClusterId": { "type": "string" }, "kind": { "description": "Fleet fields (additive): \"managed\" (Visor-provisioned) vs \"byo\" (attached\nkubeconfig), and the live GPU inventory a BYO cluster reports.", "type": "string" }, "name": { "type": "string" }, "nodeCount": { "type": "integer" }, "nodePools": { "items": { "$ref": "#/components/schemas/nodePoolView" }, "type": "array" }, "nodeSize": { "type": "string" }, "nvidiaGpu": { "type": "integer" }, "region": { "type": "string" }, "status": { "type": "string" } }, "type": "object" }, "createClusterReq": { "properties": { "name": { "description": "Name is the cluster's name. Required.", "type": "string" }, "nodePool": { "properties": { "count": { "type": "integer" }, "name": { "type": "string" }, "size": { "type": "string" } }, "type": "object" }, "region": { "description": "Region is the provider region slug (e.g. \"nyc3\"). Required.", "type": "string" }, "version": { "description": "Version is the Kubernetes version slug; empty takes the provider default.", "type": "string" } }, "type": "object" }, "engineAdvertisement": { "properties": { "apis": { "description": "[\"openai\",\"anthropic\"]", "items": { "type": "string" }, "type": "array" }, "models": { "description": "ids from the node's GET /v1/models", "items": { "type": "string" }, "type": "array" }, "status": { "description": "\"ready\" | \"unreachable\"", "type": "string" }, "url": { "type": "string" } }, "type": "object" }, "fleetBoard": { "properties": { "units": { "description": "Units is the union across sources — agent run-targets, BYO workers, BYO\nclusters and Visor machines — each row naming the source it came from.", "items": { "$ref": "#/components/schemas/fleetUnit" }, "type": "array" } }, "type": "object" }, "fleetMetrics": { "properties": { "at": { "type": "string" }, "gpuUtil": { "type": "number" }, "load1": { "type": "number" }, "memFree": { "type": "integer" }, "memUsed": { "type": "integer" } }, "type": "object" }, "fleetSpec": { "properties": { "arch": { "type": "string" }, "cpus": { "type": "integer" }, "gpuModel": { "type": "string" }, "gpus": { "type": "integer" }, "memory": { "type": "integer" }, "os": { "type": "string" } }, "type": "object" }, "fleetUnit": { "properties": { "host": { "type": "string" }, "kind": { "type": "string" }, "label": { "type": "string" }, "metrics": { "$ref": "#/components/schemas/fleetMetrics" }, "queued": { "type": "integer" }, "running": { "description": "Running is what the unit is actively executing: agent sessions for a run-target,\nin-flight renders for a BYO GPU. Queued is the gpu-jobs backlog on this GPU's\nlane (BYO units only; an agent unit does not queue). Both come from the org's\ngpu-jobs queue for BYO units, overlaid in listFleet.", "type": "integer" }, "sessions": { "type": "integer" }, "source": { "type": "string" }, "spec": { "$ref": "#/components/schemas/fleetSpec" }, "status": { "type": "string" }, "unit": { "type": "string" } }, "type": "object" }, "gpuAlertList": { "properties": { "alerts": { "description": "Alerts is always empty, and typed as a raw list because Visor exposes no\nalert inventory for this surface to shape: there is nothing to describe\nuntil there is something to return.", "items": { "type": "object" }, "type": "array" } }, "type": "object" }, "gpuJob": { "properties": { "attempt": { "type": "integer" }, "closeTime": { "type": "string" }, "failureCause": { "type": "string" }, "gpu": { "type": "string" }, "id": { "type": "string" }, "label": { "type": "string" }, "lastHeartbeat": { "type": "string" }, "leaseExpiry": { "type": "string" }, "runId": { "type": "string" }, "startTime": { "type": "string" }, "status": { "description": "queued|running|completed|failed|canceled", "type": "string" }, "type": { "type": "string" }, "worker": { "type": "string" } }, "type": "object" }, "gpuList": { "properties": { "gpus": { "description": "GPUs is every accelerator the org has, from Visor GPU droplets and from BYO\nworkers alike.", "items": { "$ref": "#/components/schemas/gpuView" }, "type": "array" } }, "type": "object" }, "gpuView": { "properties": { "id": { "type": "string" }, "location": { "type": "string" }, "machine": { "type": "string" }, "memory": { "type": "string" }, "model": { "type": "string" }, "name": { "type": "string" }, "provider": { "description": "Provider distinguishes a BYO accelerator (\"byo\") from a Visor-provisioned\none (the machine's real provider). Memory is VRAM when known (BYO reports it\nfrom nvidia-smi; Visor's machine object carries none, so it stays empty and\nthe UI renders \"—\"). Both are additive + omitempty: existing rows are\nunaffected and the console normalizer ignores fields it does not read.", "type": "string" }, "region": { "type": "string" }, "status": { "type": "string" } }, "type": "object" }, "jobCancel": { "properties": { "id": { "description": "ID is the job (activity) id, from the URL path.", "type": "string" }, "reason": { "description": "Reason is recorded on the cancellation; empty records \"canceled from console\".", "type": "string" }, "run": { "description": "Run is the run id; empty defaults to the job id, which is what the dispatcher\nsets (runId == activityId == prompt_id), so the common case sends no body.", "type": "string" } }, "type": "object" }, "jobCanceled": { "properties": { "canceled": { "description": "Canceled is the job id that was canceled.", "type": "string" }, "run": { "description": "Run is the run id the cancel was applied to.", "type": "string" } }, "type": "object" }, "jobList": { "properties": { "jobs": { "description": "Jobs is the queue, most-recent-first. Every LIVE job is present; terminal\nhistory is capped, so a busy org's running work is never crowded out.", "items": { "$ref": "#/components/schemas/gpuJob" }, "type": "array" } }, "type": "object" }, "machineList": { "properties": { "machines": { "description": "Machines is every machine the org has: Visor-provisioned and BYO together.", "items": { "$ref": "#/components/schemas/machineView" }, "type": "array" } }, "type": "object" }, "machineView": { "properties": { "createdTime": { "type": "string" }, "gpu": { "type": "string" }, "id": { "type": "string" }, "image": { "type": "string" }, "mem": { "type": "string" }, "name": { "type": "string" }, "os": { "type": "string" }, "privateIp": { "type": "string" }, "provider": { "type": "string" }, "publicIp": { "type": "string" }, "region": { "type": "string" }, "status": { "type": "string" }, "type": { "type": "string" }, "vcpu": { "type": "integer" } }, "type": "object" }, "nodeList": { "properties": { "nodes": { "description": "Nodes is one row per worker node, in the SAME machineView shape the machines\nsurface emits — a node IS a machine.", "items": { "$ref": "#/components/schemas/machineView" }, "type": "array" } }, "type": "object" }, "nodePoolView": { "properties": { "autoScale": { "type": "boolean" }, "count": { "type": "integer" }, "maxNodes": { "type": "integer" }, "minNodes": { "type": "integer" }, "name": { "type": "string" }, "poolId": { "type": "string" }, "size": { "type": "string" } }, "type": "object" }, "poolCreate": { "properties": { "autoScale": { "description": "AutoScale turns the provider's cluster autoscaler on for this pool.", "type": "boolean" }, "clusterId": { "description": "ClusterID is the cluster to add the pool to, from the URL path.", "type": "string" }, "count": { "description": "Count is how many nodes the pool starts with.", "type": "integer" }, "maxNodes": { "type": "integer" }, "minNodes": { "description": "MinNodes and MaxNodes bound the autoscaler; they are ignored unless\nAutoScale is set.", "type": "integer" }, "name": { "description": "Name is the pool's name.", "type": "string" }, "provider": { "description": "Provider is the cloud the cluster lives on (e.g. \"digitalocean\"). Required —\nVisor routes the create by it. Accepted from the body or ?provider=.", "type": "string" }, "size": { "description": "Size is the provider size slug for each node (e.g. \"s-4vcpu-8gb\").", "type": "string" } }, "type": "object" }, "poolScale": { "properties": { "clusterId": { "description": "ClusterID and PoolID address the pool, from the URL path.", "type": "string" }, "count": { "description": "Count is the node count to scale TO — an absolute target, not a delta, and\nnever negative.", "type": "integer" }, "poolId": { "type": "string" }, "provider": { "description": "Provider is the cloud the cluster lives on. Required; body or ?provider=.", "type": "string" } }, "type": "object" }, "sampleAccepted": { "properties": { "recorded": { "description": "Recorded is always true: the response is an acknowledgement, and the\nwarehouse write is detached, so it reports acceptance, not durability.", "type": "boolean" } }, "type": "object" }, "sampleIngest": { "properties": { "gpuModel": { "type": "string" }, "gpuUtil": { "description": "GPUUtil is accelerator utilization as a fraction 0..1; the warehouse clamps\nanything outside that.", "type": "number" }, "gpus": { "description": "GPUs is how many accelerators the reading covers, GPUModel the representative\nmodel name.", "type": "integer" }, "host": { "description": "Host is the node's hostname, for display.", "type": "string" }, "memFree": { "type": "integer" }, "memUsed": { "description": "MemUsed and MemFree are host memory in bytes.", "type": "integer" }, "unit": { "description": "Unit is the reporting node's own id — the same id it registered under, and\nthe key the board joins this series onto. Required.", "type": "string" } }, "type": "object" }, "sampleList": { "properties": { "samples": { "description": "Samples are the readings, OLDEST first — the order a chart plots.", "items": { "$ref": "#/components/schemas/sampleView" }, "type": "array" } }, "type": "object" }, "sampleView": { "properties": { "at": { "type": "string" }, "costCents": { "type": "integer" }, "cpus": { "type": "integer" }, "gpuModel": { "type": "string" }, "gpuUtil": { "type": "number" }, "gpus": { "type": "integer" }, "host": { "type": "string" }, "kind": { "type": "string" }, "load1": { "type": "number" }, "load15": { "type": "number" }, "load5": { "type": "number" }, "memFree": { "type": "integer" }, "memUsed": { "type": "integer" }, "memory": { "type": "integer" }, "source": { "type": "string" }, "unit": { "type": "string" } }, "type": "object" }, "sourceFailure": { "properties": { "reason": { "description": "Reason is a terse, log-safe summary — never the upstream's response body.", "type": "string" }, "source": { "description": "Source is the dependency that failed, named as an operator names it.", "type": "string" } }, "type": "object" }, "workerList": { "properties": { "workers": { "description": "Workers is one row per connected BYO machine, each carrying the host's own\nreport (GPUs, driver versions, capabilities) rather than a normalized view.", "items": { "$ref": "#/components/schemas/byoWorker" }, "type": "array" } }, "type": "object" } } } }