App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:
users := app.Group("/v1/users")
users.Post("/", api.Create)
mint := users.Group("/mint").With(middleware.Mint(gate))
mint.Post("/deposit", api.Deposit)
A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.
That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.
Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.
Co-authored-by: Hanzo Dev <[email protected]>
zap-proto/http renamed its package (v0.3.3): the module path already says http,
so zaphttp said it twice and built a compound word into every file that touched
the wire. This module imports it under its own name now.
Co-authored-by: Hanzo Dev <[email protected]>
forward() overwrote the Host header with the DIAL ADDRESS on every proxied
request. A client dials its own address — HostClient.Addr, zaphttp.Dial(addr) —
and never reads this header to find the far end, so that value was not routing
anything. It was destroying the one fact a child needs to serve more than one
brand: which host was asked.
hanzoai/iam is where it showed. It serves every brand from one instance and
resolves each brand's OIDC issuer from the request Host, so mounted as a plugin
child it was asked as "/var/lib/cloud/run/iam.sock", matched no brand, and fell
back to its single default — lux.id and zoolabs.id minting tokens claiming
iss=hanzo.id, which their own relying parties reject. Config could not reach it:
the brand map was in the child's environment and the Host never arrived.
The dial address stays the fallback for a request carrying no Host of its own
(the MCP hop builds a fresh one), because HTTP/1.1 requires the header.
Read through req.Header.Host() rather than req.Host(): the latter goes through
the URI, which parses on first touch — 34 allocations on a hop that is meant to
cost none, which the alloc gate catches. And a request that already has a Host
is now left ALONE rather than re-set.
The test measures the SERIALIZED request. fasthttp writes the Host header from
the URI once the URI has been parsed, so reading either field in memory can
disagree with what actually goes over the wire; serializing is the only reading
that cannot. It fails on the old code and passes on this one.
Also: every test that binds a unix socket now does it in a short directory.
A socket address is 104 bytes on Darwin and t.TempDir spends most of them on the
test's own name, so 9 tests failed with "never began listening" — which reads as
a timing bug, is green on Linux, red on every Mac, and breaks when a test is
RENAMED. Suite goes 11 red to 2, both of which are pre-existing and platform:
SO_PEERCRED (no LOCAL_PEERCRED path on macOS) and the alloc gate, which fails on
pristine v1.27.1 when run in isolation.
Co-authored-by: Hanzo Dev <[email protected]>
OPS_PORT and ZIP_OPS_ADDR were two environment variables for one listener, and
they disagreed. OpsAddr() turned OPS_PORT=9090 into the bare string ":9090",
and a bare address takes DefaultScheme — ZAP. So the knob HIP-0119 §5 actually
names bound a ZAP socket for /healthz, /readyz and /metrics, whose only two
clients are a kubelet httpGet probe and a Prometheus scrape, neither of which
speaks it. The probe's "GET " arrived as a frame header:
zaphttp: read frame: frame size 1195725856 exceeds MaxFrameSize=67108864
0x47455420. The other variable, undocumented, worked. That is how iam standalone
came up with no reachable /healthz, never went Ready, kept zero endpoints, and
took every caller that resolves identity through it down with it.
Both variables go. The ops listener is Config.OpsAddr — an address, in the same
grammar App.Listen already takes, so a binary states all of its listeners one
way and one place:
svc serve --zap :9653 --http http://:8000 --ops http://:9090
DefaultOpsAddr replaces DefaultOpsPort and carries the scheme, because the port
was never the part that was ambiguous. Empty still binds nothing: a host names
no ops address for a child, so a child owns none.
TestOpsListenerSpeaksHTTP pins it — it fails on DefaultOpsAddr = ":9090".
Co-authored-by: Hanzo Dev <[email protected]>
HIP-0106's plugin contract, in the framework rather than in 116 repositories.
Declaration projects the LIVE ROUTER — every method+pattern, complete by
construction, so a route a plugin serves and does not publish is still
declared. That is the analytics-ingestion outage class: the published paths
were listed, the four ingestion doors were not, and every product beacon in
the fleet answered 405.
Declaration/Route/(*App).Declaration what a host discovers
/.well-known/zip/plugin.json the same value off a running child
(*App).Described `<bin> openapi <f>` / `<bin> declare <f>`
Tenant the tenancy read, with the real rule
Delegate a continuation for another org
Ask dial a peer by name, invoke one op
(*App).Peer peer ops on their own app, own socket
(*App).Ops /healthz + /readyz + /metrics
Config.Eager the one fact a router cannot show
App.claim a second claim of a prefix is an ERROR
zip/runtime -> zip/js a language runtime is an adaptor
Four things the reference plugin falsified while being built:
- Describe was already taken. zip.Describe(methodPath, Doc) is the doc
registry generated code calls. The projection writer is (*App).Described,
one call, because argv is read in one place and a main cannot forget to
forward it.
- zip's control plane is NOT all under /.well-known/. /docs and the MCP path
are not, so a prefix filter let a child declare them — paths outside /v1/
that every plugin serves and whose first claimant takes the HOST's document
and agent door. App.control now registers and records in one call, so
leaking one is impossible rather than unlikely.
- OPS_PORT appeared in no Go file in the fleet. HIP-0119 named the paths and
nobody implemented them, so conformance meant 116 repos hand-rolling three
routes and a listener. It is one sibling app now, brought up by Listen.
- There is deliberately NO default ops port. A default made every second app
in a process die on "address already in use" — a plugin has two apps in it
and a test process has many. The deployment states OPS_PORT; a host states
none on a child, so a child correctly owns none.
Load's duplicate claim used to return nil while the first registration kept
answering: the losing plugin's whole surface served another plugin's 404 and
nothing said so. It is now an error naming both claimants, all-or-nothing
across a plugin's prefixes, taken before anything is spawned.
a.ops was the op registry and is now a.registry — the ONE registry the
OpenAPI document, the MCP tool, the CLI command and the plane op are all
projected from. a.ops is the ops sibling.
Co-authored-by: Hanzo Dev <[email protected]>