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 ¶
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 ¶
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 ¶
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 ¶
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.