8 Commits
Author SHA1 Message Date
zeekay 5085333c13 Keep every existing op's wire and close the new kinds' edges
A JSON answer keeps fiber's application/json; charset=utf-8 unless the
op names another media, and every declared status carries the Out's
schema again, a union's alternative filed under the status it states.
A value of the op's own type stating status zero is refused as before;
only Body and Redirect read zero as the op's own. A header field is a
CLI flag under its field's name as it was, and a 3xx with a body is an
answer the remote CLI prints.

form: tags bind a form only when the op consumes a form media type, so
another binder's tags no longer move an op off JSON or refuse a GET.
A cookie: field is set by the request's cookie and nothing else, and no
body schema lists it, so a cross-site body cannot fill it.

Sse.Send takes a ctx that ends with the stream: a tool call collects
for at most toolWait and toolBound bytes, then cancels the producer,
and a stuck producer cannot hold the call. A panicking Reader ends its
answer, not the process. A socket op refuses a request that cannot
upgrade before its handler runs, over REST and the call plane, and a
nil CheckOrigin is same-origin, refused 403. The call plane carries a
union's JSON alternative; Sse and Socket fields that cannot cross say
so with zap:"-". MCP bytes are bounded and untyped bytes take the op's
media.
2026-10-09 09:42:01 -07:00
zeekay 999e2153b3 Type every wire: bytes, forms, streams, sockets, redirects, unions
A typed op could only take one JSON value and answer one, so uploads,
signed webhooks, form posts, YAML and merge-patch bodies, PDFs, SSE,
WebSockets, OAuth redirects and upstream relays stayed untyped routes
with no schema, tool, command or SDK method. Each is now a typed op and
its REST wire is unchanged.

Requests: a zip.Body field takes the body and Content-Type exactly as
sent; an In implementing Parser decodes itself; form: fields and
*zip.File parts bind url-encoded and multipart forms; cookie: binds a
request cookie as an in: cookie parameter. Consumes names the media.

Answers: zip.Body (bytes, filename, its own status and headers, a
Reader streams chunked), Verbatim[T] (a relay documented as T), Sse[T]
(events with keep-alive), Socket[M] (an upgrade after the contract
runs; 426 without one, 501 over ZAP), Redirect, CookieCoder, Produces,
and unions: a type whose OneOf method's results are the alternatives,
with Or[A,B] as the stock one filing each under the status it states.

Projections follow from the manifest: the document publishes binary
media, form schemas, x-events, x-socket with 101, 3xx with Location
and oneOf; MCP takes bytes as base64 and returns bytes, text or
collected events as tool content, listing a socket op in _meta as no
tool; the CLI reads --body @file|-, --field name=@file and --out,
prints events as they arrive and bridges a socket to stdin and stdout.
tools/list is ProjectMCP over the manifest, the projection MCPTools
already was.

Prose reaches promoted fields (filed under the declaring struct), a
type defined over another struct, and the type arguments of zip's
kinds and OneOf results; an instantiated generic is published under
its name followed by its argument names instead of an illegal
component key. The unused reflect-only document and tool builders go.
2026-10-09 09:42:00 -07:00
zeekayandHanzo Dev 5f71d2496b The router is three nouns, and every one of them is concrete
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]>
2026-09-17 01:39:17 -07:00
zeekayandHanzo Dev 78b2cfe767 the transport imports as http, unaliased
zap-proto/http is package http now (v0.3.5) — the last element of its own module
path, which is the name Go resolves an unaliased import to. My v1.27.4 called it
zap, and that was wrong twice: it did not match the module path, and this package
imports zap, the wire format, so one word meant two things in the same file.

No file here imports net/http, so the transport needs no alias at all.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-10 16:19:43 -07:00
zeekayandHanzo Dev 890d5c6538 the transport is zap, not zaphttp
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]>
2026-08-10 15:39:51 -07:00
zeekayandHanzo Dev 4403d199ce feat: App.Plugins() — a host reports what it is actually running
A fleet view needs one thing this package did not expose: the ability to ask a
live process what it loaded, at which version, and whether it is still up.
Deployment config answers what was INTENDED; only the process knows what is
TRUE — which is exactly the difference that matters mid-rollout, when the two
disagree by design.

    for _, p := range app.Plugins() {
        // Name, Prefix, Source, Version, Addr, PID, Running, Since, Reloads
    }

Version is the artifact's SHA-256 for a URL-installed plugin — the only version
identifier that cannot drift from the bits running, because it IS the bits.
Since resets on Reload, so it reports the age of what is running rather than of
the mount, and Reloads climbing on one host and not its peers is the signal that
a rollout is uneven. An Unloaded plugin stays listed with Running=false, because
"deployed but down" and "not deployed" are different states and its routes are
still registered answering 503.

Remote (Addr) mounts are recorded too. They took an early return and were
invisible before; a fleet view that silently omits part of the surface is worse
than none.

Two fixes found by having the tests exercise this:

Shutdown now waits for instances a Reload or Unload is still draining. Those
retire asynchronously so the caller is not blocked for the drain window, which
let a child outlive the host that spawned it — and since children inherit the
host's stdout, the process appeared to hang after main returned. The suite
showed it as "Test I/O incomplete 1m0s after exiting"; it now finishes in 22s
instead of 74s.

Tests no longer hardcode ports. A fixed 19653 meant an unrelated process on the
box made TestListen_ZAP fail with 404 rather than a connection error, because
the port answers — it is just answering for someone else. That reads as a
routing bug and is not one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 12:51:44 -07:00
zeekayandHanzo Dev 748f8b41d1 feat(transport): unix sockets on the zap scheme — the address names the plumbing
A colocated plugin wants a socket path, not a port: nothing to allocate,
nothing to collide, and filesystem permissions are the ACL. That needed no new
scheme. The scheme names the PROTOCOL (zap); the address already says where it
is spoken, so a leading "/" (or "./", or "@" for a Linux abstract socket) is a
unix socket and anything else is host:port.

    app.Listen("/run/hanzo/billing.sock")               // in the plugin
    app.Mount("/v1/billing", "/run/hanzo/billing.sock") // in the host

A "zapuds" scheme was the obvious move and the wrong one: it is a compound name
for the same wire, and it would have split one protocol across two registry
entries that then drift. Reading the network off the address keeps it at one.

Both directions gained unix from a single registry change, which is the point
of Serve and Dial living in one Transport value.

Requires zap-proto/http v0.2.2 (network as a value).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 09:07:56 -07:00
zeekay 5e90c2aa7b feat: streaming over ZAP through the framework (zap-proto/http v0.2.0)
Bump zap-proto/http v0.1.0 -> v0.2.0 (streamed responses). Now ANY zip route that
uses c.SendStreamWriter (SSE) pushes each flush over ListenZAP as a live stream —
real server→client push over ZAP, wired through the framework with zero per-handler
transport code. This makes bidirectional/streaming (MCP notifications, progress,
chunked bodies) available to every zip app for free.

Verified: TestListenZAP_Streams flushes 3 SSE events one-at-a-time through a zip
route and the client receives each BEFORE the next is released (buffering would
time out). All transport + MCP tests pass; build/vet/gofmt clean.
2026-07-02 00:45:37 -07:00