HIPsHanzo Proposals
Back to HIPs
HIP-1107FinalStandards TrackInfrastructure

Bot — A Run on a Surface

Hanzo AI
Created: 2026-08-20
Requires: HIP-0026, HIP-0106, HIP-0135, HIP-0139

HIP-1107: Bot — A Run on a Surface

Abstract

/v1/bot is a bot doing your work on a surface this platform operates — a task executing on a desktop or terminal sandbox, with a live session a person can open to watch or take over — together with the endpoint onto the service that executes it. It is implemented in hanzoai/cloud at apps/bot (HIP-0106).

A bot is not a machine. The org's own connected machines are node (HIP-1325): a node dials in and holds a socket, and what runs on it is a command it declared it can run. A bot MACHINE is a third thing again — a box you RENT, at /v1/visor/compute/bots (HIP-1172). Three values, three names, three addresses.

Motivation

A run has one home, and it is not here. The executor holds the sandbox, so it is the only truthful answer to "what is running". A control plane that kept a second copy of that state would own a second id space agreeing with nothing: listing runs that do not exist, and stopping runs that were never started. So this capability is the shape a control plane is — hold who you are, which org you are, and whether you may, and ask the thing that actually holds the state.

Specification

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

§1 The store

No store at all.

The run registry is the executor's, keyed under the tenant that started the run, and the id space is the executor's too: cloud does not mint a run id, does not persist one, and could not resolve one it invented. That is exactly why the list and the stop agree — both speak the only id space that has ever held a real run (apps/bot/wire.go).

The one value derived here is the session address. It is built from the run id against a browser-facing gateway base, so the executor never has to know its own public origin, and that base is a separate knob from the in-cluster address the control plane calls: a session a browser embeds must be publicly reachable, and a pod-internal name is not (apps/bot/run.go).

§2 The addresses

Two route families under one prefix, which is what makes one router safe here.

The run plane. GET /v1/bot/runs answers {bots:[{runId, task, surface, status, sessionUrl, startedAt}]}. The array is always present, so an org with no runs serializes as {"bots":[]} and never as null; status is the executor's word, and running when it names none. POST /v1/bot/runs/{runId}/stop answers {runId, status} with the run's terminal state, and the run id is URL-borne only — it has never been accepted in a body. Both are typed.

POST /v1/bot/runs is declared with prose beside the route and answers 501 to every call. It cannot be a value because it has no success to publish: a typed operation declares a 200 body, and typing this one would declare a body it can never send, then mint a tool in the agent list and a command in the CLI for an operation that cannot succeed — which a model reading the tool list will call. It is also body-tolerant, which a typed operation cannot express: the handler never reads the body, so any bytes at all, malformed JSON included, get the same 501, where a typed operation refuses an unparseable body before the handler runs. The launch is a POST to the collection rather than a run literal beside {runId}, so the method says the verb (HIP-0128 §1) and there is no literal to out-rank its parameter sibling.

A launch operation, when it exists, MUST NOT mint a run id the executor has not acknowledged, MUST NOT hand back a session address for a session that does not exist, and MUST NOT charge for either. It gets typed in the change that can prove a bot boots, and not before.

The executor's ops face. /v1/bot/runtime/* relays @hanzo/bot's own operational paths verbatim, the prefix stripped on the way out (apps/bot/relay.go). A liveness probe is not a tenant-scoped resource, so it stays a relay rather than being reimplemented in Go, and it is not a public address: the public rule drops a relay endpoint (HIP-0135), so it reaches no generated client and no tool list. The segment is load-bearing. The relay was once All("/v1/bot/*") in a second app, a greedy wildcard over the whole of a sibling's subtree held apart only by two manifest rows and specificity; from /v1/bot/runtime it cannot reach a sibling at all, and since the machine plane left for its own binary (HIP-1325) there is no sibling here for it to reach.

§3 The boundary

nodes (HIP-1325) owns the machine an org already owns and connects. visor (HIP-1172) owns the bot MACHINE — a box you rent, at /v1/visor/compute/bots. This capability owns the bot RUN. Three values, three names, and the schema namespace is flat, which is why the row type here is qualified rather than called bot.

coding dispatches its own tasks to the same executor over the same transport. That is a shared road, not a shared surface: each caller owns its own wire contract, and the transport is forbidden to learn what a run is. The moment it does, it has stopped being a transport (apps/bot/transport.go).

§4 Tenancy

Every route takes its org from the gateway's verdict — injected after IAM validation, after stripping any client copy (HIP-0026) — never from a body, query or path, because a caller that could name an org could attach a machine into someone else's tenant, or halt their work. Without a validated principal the request is refused rather than defaulted. A foreign run id resolves under the CALLER's org, where it does not exist, and answers absent — so the surface is not an existence oracle. A run id is bounded before it is sent onward; an oversize id is not a run this org owns.

The org is also what cloud sends the executor, which keys every run under the tenant it names, so the boundary is enforced twice by two parties and takes no caller input at either.

Failure is reported honestly in both directions, because the two answers are different claims. An executor that cannot list is 502, never []: an empty array says "your org has no runs", and "we could not ask" is a different fact. Absence is honoured on a stop only when the executor ANSWERS absent; an executor that does not serve the operation at all has reported nothing about the run, and reporting stopped on that basis would be a stop that cannot fail (apps/bot/transport.go).

§5 Money

Free, and said in those words: the surface declares cloud.Free (plugin/bot/main.go). Nothing on it debits any plane, and no per-run fee is taken anywhere. A price belongs in the same change that can prove a bot boots.

§6 Events and observability

It publishes nothing on the bus; a customer's webhooks receive no bot.* event. Beyond the request span every route gets, it emits structured log lines only.

§7 Stage

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

§8 Upstream

It derives from no third-party OSS project. The executor is a separate Hanzo service reached over one transport, which resolves the base address, mints the server-originated identity for a call cloud makes, frames the stream, bounds a call, and decides whether a cleartext hop is allowed. Today those bytes move over HTTP; moving them to ZAP (HIP-0106) is a change to that one file plus each caller's stub.

Rationale

The alternative to asking the executor is mirroring its registry here, which buys a faster list and pays for it with a second id space, a reconciliation loop, and a stop that can address a run the sandbox never had. The alternative to 501 is a plausible launch — an id, a session address, a receipt — for a bot that never booted. A refusal a caller can act on is worth more than a success it cannot.

Security Considerations

This capability is destructive rather than merely readable. If the org came from a request field or a client header, one tenant enumerates another's runs and then halts them: a stop is not a disclosure, it is the loss of somebody's work in progress. And if a launch minted an id the executor had not acknowledged, the caller would hold a session address pointing at a node that does not exist, and any price attached to that call would be money taken for nothing. §2 forbids both.

The relay is the other edge. It refuses before forwarding, because the executor trusts the identity headers it receives as gateway-minted — an unauthenticated call allowed through would hand it a victim tenant. It forwards the caller's own headers and mints nothing, bounds the body it reads back, and returns the executor's status and content type unchanged.

References

  • HIP-0026 — Identity and Access Management
  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-0128 — Resource Surface Standard — Generated REST over ZAP
  • HIP-0135 — What Is Public
  • HIP-0139 — Capability
  • HIP-1172 — visor — Compute You Rent
  • HIP-1325 — Nodes — Your Machines on a Socket

Copyright

Released under CC0 1.0 Universal Public Domain Dedication.