mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
Three things a consumer migration surfaced. HEAD shadows. Declaration filtered ALL HEAD and OPTIONS, which also deleted an EXPLICITLY declared HEAD — a real door a host would then answer 405 on, which is the analytics-outage class this type exists to prevent. The doctrine was right and the mechanism was wrong: a declaration now projects the PROGRAM, not the materialised router, so shadows never appear (nothing wrote them) and declared HEAD does. App.All is the one entry that is genuinely many doors, so it expands. Consumers enumerating routes should read Declaration, not Fiber().GetRoutes — fiber's own method cannot apply a doctrine zip owns. Fiber() escapes. Fiber() hands out the CURRENT generation's router, and a generation is a projection, so app.Fiber().Use(cors.New(...)) was silently discarded by the next build — no error, no CORS headers. A doc line is weak medicine for a lost CORS policy, so the generation records its route count and a rebuild that finds more refuses, naming the count and pointing at app.Use. Listen. Serve is the one serving mechanism and Listen is now literally Serve followed by Wait — sugar, not a second path. The no-handle shape is deliberate and documented: a fixed program wants the terminal form, anything that loads at run time wants the handle. And a bug of my own, caught by TestLoad_EmbeddedBinary: the first cut of Serve left h.errc nil, so Wait returned immediately and Listen stopped serving at all. The test I wrote for Listen passed against that, so it has been strengthened to dial the socket and assert Listen has NOT returned — verified to fail without the fix. Co-authored-by: Hanzo Dev <[email protected]>
116 lines
4.3 KiB
Go
116 lines
4.3 KiB
Go
// Command testplugin is a minimal zip plugin used by the reload tests. It
|
|
// serves one route reporting the version stamped in at link time, so a test can
|
|
// prove WHICH build answered — the only honest way to show a reload swapped
|
|
// processes rather than just restarting the same one.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// version is set with -ldflags "-X main.version=…" so two builds of identical
|
|
// source are distinguishable on the wire.
|
|
var version = "unset"
|
|
|
|
// Version reports which build is answering.
|
|
type Version struct {
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// Crashing acknowledges a crash request before the process dies.
|
|
type Crashing struct {
|
|
Crashing string `json:"crashing"`
|
|
}
|
|
|
|
// Nothing is an operation that takes no input.
|
|
type Nothing struct{}
|
|
|
|
// tenant is a per-caller tool source: the half of a door that cannot be
|
|
// projected, because the answer depends on WHO is asking. A host that declares
|
|
// this plugin Open asks it per caller, and this is what answers.
|
|
type tenant struct{}
|
|
|
|
func (tenant) Tools(ctx context.Context) []map[string]any {
|
|
org := orgOf(ctx)
|
|
if org == "" {
|
|
return nil
|
|
}
|
|
return []map[string]any{{
|
|
"name": org + "_own", "description": "a tool only " + org + " has",
|
|
"inputSchema": map[string]any{"type": "object"},
|
|
}}
|
|
}
|
|
|
|
func (tenant) Call(ctx context.Context, name string, _ json.RawMessage) (any, error) {
|
|
if org := orgOf(ctx); org != "" && name == org+"_own" {
|
|
return map[string]any{"ran": name, "org": org}, nil
|
|
}
|
|
return nil, zip.Errorf(404, "unknown tool: %s", name)
|
|
}
|
|
|
|
// orgKey carries the caller the door is serving, parked by the middleware below
|
|
// the way a host's identity boundary does.
|
|
type orgKey struct{}
|
|
|
|
func orgOf(ctx context.Context) string { org, _ := ctx.Value(orgKey{}).(string); return org }
|
|
|
|
func main() {
|
|
app := zip.New(zip.Config{AppName: "testplugin", DisableStartupMessage: true,
|
|
MCP: zip.MCPConfig{Source: tenant{}}})
|
|
app.Use(zip.H(func(c *zip.Ctx) error {
|
|
c.SetContext(context.WithValue(c.Context(), orgKey{}, c.Org()))
|
|
return c.Continue()
|
|
}))
|
|
|
|
// Typed, like any other route worth having: a plugin is an ordinary zip
|
|
// app, so its ops project into the host's document, tools and call plane
|
|
// exactly as a linked-in service's do.
|
|
zip.Get(app, "/v1/demo/version", func(context.Context, *Nothing) (*Version, error) {
|
|
return &Version{Version: version}, nil
|
|
}, zip.WithSummary("Reports which build of the plugin is answering."))
|
|
|
|
// Crashes the process the way a real bug does — a panic on a goroutine,
|
|
// which no handler recover() can catch. Used to prove the host survives a
|
|
// plugin dying and brings it back.
|
|
zip.Get(app, "/v1/demo/crash", func(context.Context, *Nothing) (*Crashing, error) {
|
|
go func() { panic("testplugin: deliberate crash") }()
|
|
return &Crashing{Crashing: "true"}, nil
|
|
}, zip.WithSummary("Crashes the plugin process, to prove the host survives it."))
|
|
|
|
// Untyped on purpose, and the one route here that has to be. It echoes
|
|
// whatever path reached it, so a host that mounts this plugin at more than
|
|
// one prefix can tell "the second prefix is not mounted" (the HOST 404s)
|
|
// from "the plugin has no such route" (the PLUGIN 404s) — which are
|
|
// otherwise identical from the outside. A wildcard answers paths that do not
|
|
// exist, so there is no operation for it to be: an op is one named endpoint
|
|
// with one schema, and this is deliberately neither.
|
|
app.All("/*", func(c *zip.Ctx) error {
|
|
return c.JSON(200, map[string]string{"echo": c.Path(), "version": version})
|
|
})
|
|
|
|
// `testplugin tools` prints this plugin's MCP catalogue instead of serving —
|
|
// the build-time step a host's Plugin.Tools is captured from, and the same
|
|
// thing hanzoai/cloud's `<app> describe` does. Projecting it is the whole of
|
|
// what a plugin does to be on a host's composed MCP door.
|
|
if len(os.Args) > 1 && os.Args[1] == "tools" {
|
|
b, err := json.Marshal(app.MCPTools())
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
_, _ = os.Stdout.Write(b)
|
|
return
|
|
}
|
|
|
|
// Addr is the whole plugin side of the contract: serve where the host said.
|
|
// Report rather than swallow: a fixture that discards this error exits
|
|
// silently and the host reports only "exited before listening".
|
|
if err := app.Listen(zip.Addr(":9999")); err != nil {
|
|
log.Fatalf("testplugin: %v", err)
|
|
}
|
|
}
|