HIPsHanzo Proposals
Back to HIPs
HIP-1325FinalStandards TrackInfrastructure

Node — A Machine an Org Owns

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

HIP-1325: Node — A Machine an Org Owns

Abstract

/v1/node is an org's own machines. An agent on someone's machine dials in and holds a socket; the org lists what is connected and asks one of them to run a command, authorized once at the socket against the deployment's own allowlist. It is implemented in hanzoai/cloud at apps/node (HIP-0106).

A node is not a bot. A bot is an agent bound to a machine and a bot RUN is a task the executor drives on a surface this platform operates (HIP-1107); a bot MACHINE is a box you RENT (HIP-1172). This is the machine you already own, connected. Three values, three names, three addresses.

Motivation

The machine half answered under the bot's name. It was /v1/bot/nodes while one package served both planes, then /v1/node while the package was still called bot — an address not named for the app that answered it, which is the one line cloud's openapi/misfiled.txt carried for it (HIP-0139 §5.1). The name has followed the address, and the two planes are two capabilities.

They were never one. HIP-0139 §7.2 permits a split along a store boundary and only there, and the boundary here was already drawn in the file layout: the node plane holds a presence map keyed by machine, and the run plane holds nothing at all because the executor holds it. Neither reads the other's, neither has a symbol the other uses, and neither ever did. What kept them in one package was the shared word bot, which is exactly the coincidence HIP-0139 exists to refuse.

Specification

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

§1 The store

No durable store.

A node is addressed by (org, nodeID) in an in-memory registry on the replica holding its socket — a lookup without an org cannot compile — and which replica holds which node is written to a shared presence map in Hanzo KV (CLOUD_KV_URL), because with more than one replica the socket lands on one pod and the invocations land on any.

The presence map is this capability's alone. The run plane it split from opens no store, so there is nothing shared to divide and no reconciliation between them; that is the store boundary HIP-0139 §7.2 requires, and it predates the split.

§2 The addresses

Four route families under /v1/node. GET /v1/node is typed. Three are declared with prose beside their wire facts, held closed by this capability's own projection gate — one gate per capability, because a ledger that spans two cannot say which of them a route belongs to:

GET /v1/node/connect is a WebSocket upgrade — 101 and then a duplex frame stream for the life of the node, which no typed operation can declare. POST /v1/node/{id}/invoke refuses with a domain body a client switches on, identical for a pre-flight refusal and for the node's own denial, and reads the caller's device header, which no In field may carry: a caller that could name its own device could pre-approve its own privileged command. POST /v1/node/peer/invoke is the replica-to-replica forward — plain-text refusals, and a body bound a typed operation cannot see.

The peer hop is a machine call and not a public address: the public rule drops it (HIP-0135), so it reaches no generated client and no tool list.

§3 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 run a command on one. Without a validated principal the request is refused rather than defaulted.

A node id belonging to another org MUST answer exactly like one that does not exist, so the surface is not an existence oracle, and an id is bounded before it is looked up — an oversize id is not a node this org has.

The one exception is the peer hop, whose org does arrive in a body: it is a machine call from another replica that derived the org from a validated header, and it authenticates with a shared token (CLOUD_BOT_PEER_TOKEN). Without that token the peer endpoint MUST serve nothing at all.

§4 Authorization happens once, at the socket

The registry runs the policy gate on the replica holding the node — the only one that knows what the node declared — so a locally-held node and one reached through a forward are authorized by the same code with the same session in hand. There MUST NOT be a second check at invoke time: a check in a place that sometimes has the session and sometimes does not is how two answers drift apart.

Everything a node self-reports — its capabilities, its commands, its platform — is shown and never load-bearing. What it may actually be asked to run is decided against the deployment's allowlist, where a dangerous command is reachable only through an explicit allow and a deny wins over both. A connect carrying an Origin header is refused outright: a node is a daemon, and removing the browser category entirely is the gate.

§5 Money

Free, and said in those words: the plugin declares Price: cloud.Free. Nothing on this surface debits any plane.

§6 Events and observability

It publishes nothing on the bus, so a customer's webhooks (HIP-1310) receive no nodes.* event. Beyond the request span every route gets, it emits structured log lines only.

§7 Shutdown

The presence-renew loop ends on graceful shutdown, which releases this replica's claims on the way out so peers stop forwarding into a pod that is draining. The hook belongs to this capability's binary and to no other: the run plane it split from has no loop and now declares none.

§8 Stage

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

§9 The name owes the singular sweep

The row is node, and under the rule that now governs (HIP-0139) the canonical name is the singular: this capability's row, package, plugin, floor key and tag become node. It is written plural here because that is what the manifest carries at the moment of writing, and a HIP whose capability: disagrees with the row describes a fleet nobody runs.

It is deliberately NOT renamed ahead of the sweep. Four siblings — bot, campaign, link, network — are in the identical state, and moving one of five leaves a mixed estate plus a collision with the slice that moves the rest. Nothing is broken while it waits: the derivation accepts /v1/node already, so both spellings answer and only the published spelling is pending.

§10 Upstream

It derives from no third-party OSS project. The socket wire is protocol version 3 of the @hanzo/bot TypeScript gateway, which Hanzo also maintains, implemented here frame-for-frame.

Rationale

The alternative rendezvous is the presence store's own pub/sub, publishing invokes to the holding pod's channel. It was rejected for its failure mode: a replica that died between the lookup and the publish costs the caller the whole timeout, where a direct peer forward fails fast and visibly.

The alternative to the split is the arrangement it replaces — one package, two planes, one address not named for its app and a line in the ratchet explaining it. Folding the other way, to /v1/bot/nodes, was the cheaper edit and the wrong one: it would file a machine under an agent's name permanently, in nine projections, on the argument that they once shipped in the same binary.

Security Considerations

This capability's wrong implementation is remote code execution on a customer's machine — the commands include running processes and reading devices — so every control in §3 and §4 is about who can reach a node and with what.

The org from the gateway's verdict closes cross-tenant attach and invoke. The Origin refusal closes a page riding a signed-in viewer's session into registering a node. The allowlist closes a compromised cloud asking for a command the deployment never enabled. Correlation ids minted under the connection id mean a node can only ever answer calls placed on its own socket.

The peer token is the remaining credential, and an unauthenticated peer endpoint that took an org from a body would be a cross-tenant invoke primitive — which is why, absent the token, it serves nothing.

The split itself removes one hazard and creates none. The run plane's relay is a greedy wildcard over /v1/bot/runtime/*; while both planes registered on one router, that wildcard and this capability's static leaves were held apart by specificity alone. They are now in different binaries behind different prefixes, where neither can reach the other at all.

References

  • HIP-0026 — Identity and Access Management
  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-0135 — What Is Public
  • HIP-0139 — Capability
  • HIP-1107 — Bots — Runs on a Surface
  • HIP-1172 — visor — Compute You Rent
  • HIP-1310 — Webhooks — Outbound Delivery

Copyright

Released under CC0 1.0 Universal Public Domain Dedication.