fleet

package
v1.801.413 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package fleet is how the light host answers a question about a subsystem: it ASKS the subsystem.

The host links no subsystem (cmd/cloud), so it cannot read a registry it does not hold. For a long time it read a COMMITTED PROJECTION instead — plugin/<app>/mcp.json, the tool array each app's binary wrote when it was built, embedded into the host and handed to zip as Plugin.Tools. That file was a second source for a fact the child already knows, and a second source can only be stale or accidentally correct. It was stale: plugin/o11y/mcp.json held 12 tools while the o11y binary at the same commit served 365, because the 353 missing ops live in github.com/hanzoai/o11y and a dependency bump in another repository invalidated an artifact in this one with nothing in the diff to say so.

Regenerating that file more often does not fix it. A generator on a hook is still two sources with a race between them, and the trigger here is in a different repository, so no hook in this one can see it. The fix is that the file stops existing and the host asks.

What asking costs, and why it is affordable

A child answers on its OWN door — zip's default /mcp, which cloud.Serve deliberately leaves where the framework puts it (manifest.FrameworkMCPPath) — over the private ZAP socket the host started it on. At resolves the name to that address, and zip.App.Start is exactly that function: idempotent, and the SAME single-flighted path a request to the app's prefix takes, so a burst of concurrent askers still produces one child.

So the first ask of a cold app pays that app's start. That is the cost of the answer being true, and it is paid once per app per host: the child stays up afterwards, and every later ask is a unix round trip. Nothing is cached between requests, because a cache of a catalogue IS the file this package exists to delete.

A subsystem that does not answer is REPORTED

Answer.Err is never swallowed. A catalogue that silently drops the app it could not reach is indistinguishable from one whose app serves nothing, and those are the same defect the stale file was: the caller cannot tell. Every caller here reports its failures by name — see Door for the wire shape.

Index

Constants

View Source
const Unavailable = "hanzo.ai/unavailable"

Unavailable is the _meta key under which tools/list names the subsystems it could not ask.

It exists because a short list and a stale file are the SAME defect: the caller cannot tell a subsystem that serves nothing from one that did not answer, so it reads a partial catalogue as a complete one. MCP puts extension data on the result's _meta, so the outage travels with the answer it qualifies and a client that only reads `tools` still gets every tool that exists — blanking a working fleet because one app is down would be a worse answer than a shorter list, but an UNANNOUNCED shorter list is worse than both.

Variables

This section is empty.

Functions

This section is empty.

Types

type Answer

type Answer struct {
	// App is the subsystem asked, as the manifest names it.
	App string
	// Body is what its own door replied, verbatim.
	Body []byte
	// Err is why there is no reply: it would not start, or it did not answer.
	Err error
}

Answer is one subsystem's reply to one question, or the reason there is none. Exactly one of Body and Err is meaningful.

func Ask

func Ask(at At, apps []string, req *fasthttp.Request, path string) []Answer

Ask puts req to every named app's own door at path, in PARALLEL, and returns one Answer per app in the order asked.

req is the CALLER's own request and is copied per child rather than rebuilt, so a child answers as itself for the caller who asked: its headers ride along, which is how a subsystem whose tools depend on the tenant (the tool plane's connectors, skills and enabled servers) contributes rows no projection could have held. That is the mechanism zip already used for its single "open" plugin, applied to every app, which is what makes the one-open-plugin rule unnecessary.

Order is the order given — the manifest's mount order, which is the fleet's routing order — so a caller that resolves a collision by taking the first resolves it the way the router would.

type At

type At func(app string) (addr string, err error)

At resolves one app to the address its own door answers on, starting it if it is cold.

It is a function rather than a *zip.App because there are two ways an app is reachable and only one of them is a child of this host: zip.App.Start covers the spawned ones, and a deployment that points CLOUD_<NAME>_ADDR at an instance running elsewhere is mounted, never started, so Start does not know it. The composition root holds both facts (cmd/cloud), and this package holds neither.

type Door

type Door struct {
	// contains filtered or unexported fields
}

Door is the composed agent door: the apps it fronts, how to reach one, and the tool→app routing it learned from the last time it asked.

func Mount

func Mount(host *zip.App, path string, apps []string, at At) *Door

Mount serves the fleet's agent door at path, over apps, reaching one with at.

apps is the deployment's COMPOSED set (cmd/cloud's `composed`), never the whole manifest: a deployment that does not run a subsystem must not offer its tools, for the same reason it must not publish its routes.

type Outage

type Outage struct {
	App   string `json:"app"`
	Error string `json:"error"`
}

Outage is one subsystem that could not be asked, and why.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL