The fleet's agent door answered from plugin/<app>/mcp.json: the tool array each
app's binary projected when it was BUILT, embedded by plugin/embed.go and handed
to zip as Plugin.Tools. 116 files, 49,865 lines, and a second source for a fact
every child already knows.
A second source can only be stale or accidentally correct. This one was stale in
the way no gate in this repository could see: o11y's 353 missing ops live in
github.com/hanzoai/o11y, so a go.mod bump in ANOTHER repo invalidated an artifact
in this one with nothing in the diff to say so. Regenerating it more often is not
the fix — a generator on a hook is still two sources with a race between them,
and the trigger is in a different repository. (593aa309 did regenerate it, which
is why the file reads 365 today. The next cross-repo bump silently un-fixes it.)
So the host asks. POST /v1/mcp is the HOST's own handler now (zip's is Disabled,
so exactly one handler holds the address). A tools/list forwards the CALLER's own
message to every composed subsystem's own /mcp over its private ZAP socket, in
parallel, and unions the replies — zip.App.Start resolves a cold child on the
same single-flighted path a prefix request takes, so the first list pays one
start per app and nothing after it does. A tools/call goes to the app that listed
the name, verbatim; the child's own registry decides whether the tool exists.
A SUBSYSTEM THAT DOES NOT ANSWER IS NAMED, in result._meta["hanzo.ai/unavailable"],
because a silently-short list and a stale file are the same defect: the caller
cannot tell an app that serves nothing from one that did not answer. Measured on
the built binaries — host + real o11y child, kms pointed at a dead address:
tools=364, unavailable=[{kms, connection refused}]
364 and not 365 because o11y projects get_v1_o11y_logs twice; the door serves the
first and logs the collision. THAT DUPLICATE IS WHY cmd/cloud's tests were red on
main — zip refused the Load ("tool is already served by plugin o11y"), a boot
failure. One name still has one owner; it is no longer fatal to the fleet.
Also gone with the mechanism they configured: manifest.App.Open and zip's
one-open-plugin rule. The host forwards the caller's own request to EVERY
subsystem now, so each answers for this caller out of its own rows, and being
asked per caller is no longer a privilege one app holds.
The release gate moved with the door. Car 3 compared the live tool count against
`jq -s length` over the committed files — both sides were the same bytes, so it
proved only that the image carried its own tree, and it passed while o11y's
catalogue held 12 of 365. It asks the better question now: did every subsystem
answer. A broken deployment used to match the files exactly.
plugin/<app>/openapi.json SURVIVES, for the one reason the catalogue could not:
the weave carries each subsystem's prose, and that prose is lifted from the app's
SOURCE at describe time (openapi.Synopsis). A running child has no comment to
read and would answer with its deployment's brand blurb, which the weave would
publish as the description of every product tag. Deleting that half waits on the
synopsis becoming a declared value.
Tests are against RUNNING subsystems (fleet/mcp_test.go): real zip children on
real ZAP sockets, exact sets, bodies never status codes. Mutation-checked three
ways — unmount a child, silence the outage report, ask only the first app — all
three go red.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
62 lines
3.1 KiB
Go
62 lines
3.1 KiB
Go
// Package plugin carries the fleet's build-time PROJECTION into the host: the
|
|
// OpenAPI subset each subsystem serves, known without running it.
|
|
//
|
|
// It is a function of the plugin's own router, so it is known when the plugin is
|
|
// BUILT: `<app> describe plugin/<app>` writes openapi.json from one mount of one
|
|
// router (describe.go). This package embeds those files, and the host weaves them
|
|
// into the fleet document it serves at /v1/openapi.json.
|
|
//
|
|
// It exists as its own leaf package for one reason: go:embed cannot reach outside
|
|
// its own directory, so the bytes must be embedded from HERE, and cmd/cloud must
|
|
// stay light. This file imports embed and nothing else — no apps, no cloud root —
|
|
// so `go list -deps ./cmd/cloud` gains exactly one package and still links no
|
|
// subsystem.
|
|
//
|
|
// # There is no MCP catalogue here any more
|
|
//
|
|
// It used to embed plugin/<app>/mcp.json too — the tool array the app's binary
|
|
// projected at build time — and hand it to zip as Plugin.Tools so the host could
|
|
// answer tools/list without touching a child. 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 missing 353 ops live
|
|
// in github.com/hanzoai/o11y and a go.mod bump in ANOTHER repository invalidated
|
|
// an artifact in this one with nothing in the diff to say so. No generator on a
|
|
// hook in this repo could have seen that trigger.
|
|
//
|
|
// So the catalogue is not regenerated more often; it is gone, and the door asks
|
|
// the child (package fleet). The subset below survives for the one reason the
|
|
// catalogue could not: the fleet's document carries each subsystem's PROSE, and
|
|
// that prose is lifted from the app's SOURCE at describe time
|
|
// (openapi.Synopsis) — a running child has no comment to read and would answer
|
|
// with its deployment's brand blurb instead, which the weave would then publish
|
|
// as the description of every product tag. Making the synopsis a declared value
|
|
// is what the other half of this file is waiting on.
|
|
package plugin
|
|
|
|
import "embed"
|
|
|
|
// specs holds every app's openapi.json — the same committed files the drift gate
|
|
// regenerates from source and the weave composes into openapi.yaml
|
|
// (mk/fleet.mk surface-check). Embedding them is what lets the host describe the
|
|
// whole fleet without starting any of it: the alternative is reading the live
|
|
// router, and the light host's live router is 113 proxy prefixes.
|
|
//
|
|
// A glob, so an app that has not been described yet is absent rather than a build
|
|
// failure in the host. Absence is then refused where it can be reported —
|
|
// openapi.Subsets, at the request that needs it — instead of by a compiler error
|
|
// nobody can act on.
|
|
//
|
|
//go:embed */openapi.json
|
|
var specs embed.FS
|
|
|
|
// Spec is app's own OpenAPI subset: the document its binary projected from its
|
|
// own router at build time. Nil for an app that ships none.
|
|
func Spec(app string) []byte {
|
|
b, err := specs.ReadFile(app + "/openapi.json")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return b
|
|
}
|