Documentation
¶
Overview ¶
board.go — GET /v1/fleet: the org's compute, from every source, on ONE board, each unit carrying its latest utilization.
The fleet is already visible, just never in one place: Visor's machines (/v1/machines), the BYO workers that dialed in (/v1/fleet/workers), the BYO clusters (/v1/clusters) and the agent run-targets (/v1/agents/targets) each answer for their own plane. This unions them behind the tenant's ONE question — "what compute do I have, and how hot is it?" — and overlays the utilization series (clients/samples) that no source used to keep.
GET /v1/fleet the org's units + their latest sample -> {units:[fleetUnit]}
GET /v1/fleet/samples one unit's / the org's time series -> {samples:[sampleView]}
GET /v1/fleet/workers the raw BYO inventory (fleet.go) -> unchanged
It lives in visor because visor already owns /v1/fleet/workers and the compute surface — this is that surface completed, not a rival face.
FAIL-SOFT BY SOURCE. Every source is folded in independently and a broken one contributes an empty slice, never an error: a wedged Visor, an unmounted agents subsystem or an absent warehouse each cost the board THAT source's rows and nothing else. A tenant's own dialed-in GPU must never disappear because an upstream they do not use is down. The board 500s for exactly one reason: no validated tenant.
ISOLATION: principal.Org is the ONLY tenant key, taken from the validated IAM claim (never a client field) and passed to each source's own org-scoped read.
bots.go mounts the bot-MACHINE surface (/v1/compute/bots) plus the machine agent-binding proxies (/v1/machines/:id/{bind-agent,agent-binding}, /v1/agent-bindings). It is the SIBLING of machines: a bot machine is not a new state this subsystem owns, it is a composition of two things vm already owns — a kind=bot Machine and an AgentBinding. So every route here is a thin, org-scoped translation over the SAME Visor client the machines routes use (client.go), never a second store.
The noun is the MACHINE that hosts a bot runtime — distinct from the bot RUN at /v1/bots (clients/bots), which is a task the runtime executes. Two values, two namespaces: this one nests under /v1/compute (visor's domain) because what it rents you is compute.
A bot machine = Agent (cloud /v1/agents) + Machine (vm, kind=bot) + the binding between them. Composition, one way per verb:
launch = vm POST /v1/machines/launch {kind:bot} THEN vm POST .../bind-agent
list = vm GET /v1/machines?kind=bot joined with the org's bindings
get = vm GET /v1/machines/:id joined with its binding
delete = vm DELETE .../agent-binding (unbind) THEN vm DELETE /v1/machines/:id
message = the AGENT path: run the bot's bound agent via /v1/agents/:agent/run
stop = vm DELETE .../agent-binding — halt the bot's @hanzo/bot runtime
pause = the same halt: DigitalOcean/vm expose no VM-suspend primitive, so a
bot's stop and pause are one honest capability (detach the agent
runtime); powering the underlying machine off/on is a machine-lifecycle
concern handled by launch/delete, not a fabricated bot state.
Tenancy is identical to machines: the org is the VALIDATED principal (principal.Org, taken from the IAM owner claim), forwarded to vm as ?owner=<org>, so a caller can only ever read or mutate its OWN bots. No validated principal ⇒ 403, before anything reaches vm.
client.go is the ONE HTTP path from this subsystem to Visor (the cloud OS at visor.hanzo.svc:19000 that owns compute — machines and DOKS node pools). Every handler in visor.go routes through this client, so the wire contract (base URL, auth, the casibase {status,msg,data} envelope, error mapping) lives once here and can never drift between six hand-rolled fetches.
AUTH (one rule): a request carries a Visor identity that is EITHER the service credential (VISOR_CLIENT_ID + VISOR_CLIENT_SECRET, KMS-sourced, sent as Basic auth so Visor's ApiFilter authorizes cloud as the `app/<visorApp>` subject) OR the caller's forwarded Authorization bearer when no service credential is configured. The tenant is ALWAYS pinned by ?owner=<org> (the validated principal's org, never a client field) plus the forwarded identity headers, so Visor scopes to exactly the caller's tenant on both paths.
ENVELOPE: Visor (casibase) returns HTTP 200 with {status:"ok"|"error", msg, data}. A logical failure is status:"error" at HTTP 200, NOT a 4xx/5xx — so a bare status-code check would read an error as success. call() inspects the status field and surfaces msg as an honest error; it never fabricates data.
fleet.go — BYO ("bring your own") compute: the operator's OWN machines that dialed IN via `hanzo link`, as opposed to the DOKS/DigitalOcean machines Visor provisions. A BYO worker is an outbound agent behind NAT: it can't be listed by Visor (Visor never provisioned it), so its presence lives as a heartbeating standalone activity in the org's `fleet` namespace of the ONE in-process tasks engine (cloud.EmbeddedTasks). This file reads that registry and folds it into the SAME machineView / gpuView the console already renders, tagged provider="byo", so the existing Machines and GPUs pages light up for free — no parallel UI. It also serves the raw list at GET /v1/fleet/workers.
Registration is written by the CLI over the public tasks surface (POST /v1/tasks/namespaces/fleet/activities + heartbeat) — this subsystem only READS, and only ever the caller's own tenant (principal.Org → org shard).
k8s.go is the UNIFIED /v1/k8s surface — the ONE Kubernetes noun on api.hanzo.ai: list clusters, one cluster's detail (node pools + worker nodes), DEPLOY (create) and delete DOKS clusters, and the fleet-wide worker NODES. Every route is a thin, tenant-scoped proxy to Visor (which OWNS the DigitalOcean lifecycle); this client fabricates nothing — a cluster row is a real DOKS cluster, its nodes are real droplets, and an honestly-absent field is omitted, never invented.
Surface (org taken verbatim from the validated IAM owner claim, never a client field, so a caller only ever sees or mutates its OWN tenant's clusters):
GET /v1/k8s/clusters list the org's DOKS clusters (+ BYO fold-in) -> {clusters:[clusterView]}
GET /v1/k8s/clusters/:id one cluster's detail: pools + worker nodes -> clusterDetailView (404 if absent)
POST /v1/k8s/clusters provision a DOKS cluster (ADMIN-GATED) -> clusterView (201)
DELETE /v1/k8s/clusters/:id destroy a DOKS cluster (ADMIN-GATED) -> 204
GET /v1/k8s/nodes every DOKS worker node as a machine -> {nodes:[machineView]}
READS are org-scoped (any validated member of the org). MUTATIONS (create/delete) are admin-gated — a SuperAdmin (platform sudo) OR an OrgAdmin of the owning org — because provisioning spends real infrastructure on Hanzo's house account. The gate is the SAME principal predicate the rest of the cloud mutating surface uses.
types.go holds the Visor wire structs (what upstream returns) and the console view structs (what this subsystem emits), plus the PURE mapping between them. The view JSON keys mirror the console normalizers EXACTLY so the Machines, GPUs and Clusters pages render with no front-end change:
- machineView -> console src/lib/api/visor.ts normalizeMachine
- gpuView -> console src/lib/api/compute.ts normalizeGpu
- clusterView -> console src/lib/api/platform.ts Cluster + NodePool
Every field is a REAL Visor value or an honest omission. Telemetry Visor does not carry (GPU utilization/temperature/power) is left off the gpuView so the UI shows "—", never a fabricated 0.
Package visor mounts the Hanzo Cloud COMPUTE surface: the tenant's machines, GPUs and DOKS clusters, served as clean REST off the unified cloud binary and fronting Visor (the cloud OS at visor.hanzo.svc that OWNS compute). It exists so the console's Machines / GPUs / Clusters pages read real per-org compute from ONE place (api.hanzo.ai/v1/*) instead of the god-mode /paas admin proxy that 501s until a service token is wired.
This subsystem OWNS no compute state — Visor does. It is a thin, tenant-scoped translator: it maps Visor's verb-style + resell endpoints to the clean REST the console already speaks, and re-shapes Visor's objects into the exact JSON the console normalizers consume (see types.go). It never fabricates: a GPU row is a real GPU machine's accelerator, a cluster is real node pools, and telemetry Visor does not carry is honestly omitted (renders "—"), not invented.
THE SURFACE DESCRIBES ITSELF. Every route with a request/response shape is a TYPED op (zip.Get/Post/Delete[In, Out]) registered in Mount, so the route, its In/Out schema, the prose on its handler and the doc comment on every field are ONE declaration projected into REST, /.well-known/openapi.json, the /mcp tool list and the CLI. There is deliberately no route table in this comment: a hand-kept list is a second copy, and the one that used to live here had already drifted — it was missing the whole /v1/fleet and /v1/k8s surface. Read Mount, or ask the running deployment.
Five routes stay raw, each because it has no shape to state rather than because nobody got to it: the two launches are polymorphic on the wire (a dryRun quote or a created resource), the two catalog reads pass Visor's payload through verbatim, and the bot action verb streams the agent's answer back untouched. Each carries a note at its registration.
The tenant (principal.Org) is passed to Visor as ?owner=<org>, so a caller can only ever read or mutate their OWN tenant's compute; the org is taken from the validated IAM owner claim, never a client field.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Mount ¶
Mount wires the compute surface onto app per HIP-0106. visor is a "complex" mount (it keeps a "compute"-provider meter and a fleet-scoped sub-logger that both need deps at construction), so it builds the Service value directly.
Routes are TYPED ops (zip.Get/Post/Delete[In, Out]) wherever the route has a request/response shape: a typed op is the entry in the ONE registry that REST, OpenAPI, MCP and the CLI all project from, so a route registered any other way is invisible to three of the four. The handful that stay raw are the ones with no shape to state — see the note at each.
Types ¶
This section is empty.