14 Commits
Author SHA1 Message Date
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
zeekay 4765664268 fix(host): never evict a process with requests in flight, rank recency by request end 2026-10-03 16:01:26 -07:00
zeekayandHanzo Dev 5f3753e235 Honor ctx cancellation and deadlines on every outbound call
Call, remote invokes, proxy hops and CLI commands all called Client.Do,
which takes no ctx, so a cancelled or expired caller kept waiting until
the transport's own read timeout. zap-proto/http v0.3.11 adds
Transport.DoContext; a client that implements the new ContextClient
interface is now driven through it, and Call returns an error wrapping
context.Canceled or context.DeadlineExceeded.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 00:07:13 -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
zeekay 9db2563483 Declare the wildcard segment, do not only spell it
Template names a wildcard {wildcardN} and the document builder writes that into
the path, but colonParams — which derives the parameter declarations — read
`:name` alone. So the path carried a template variable nothing declared, and the
field binding it was described as a QUERY parameter under fiber's capture key.
A client generated from that has no argument for the address, sends the braces
literally, and is answered 404 by the route it was generated for. Spelling the
segment without declaring it is worse than refusing the op, which is what this
did before.

pathParams replaces colonParams and carries both names a segment goes by: the
document's, which a parameter must be declared under because the path uses it,
and the router's, which is what an input's `url:` tag binds against. Every reader
takes the one it means, so neither can be mistaken for the other — declared under
the router's key the document would name a parameter no path contains; bound
under the document's the input would bind nothing.

It also carries the segment's index, which is the only substitution that works
for a wildcard: remote replaced `:name` by spelling, and a wildcard's segment is
`*`, matching no name.

TestAWildcardIsDECLAREDAndNotJustSpelled asks the document for both halves —
the declaration present, and the capture key absent from the query — because the
missing declaration showed up as the phantom query parameter.
2026-08-24 14:54:29 -07:00
zeekay 7afa7ee42b here: a co-resident caller runs the op, and MCP rides zap-proto/mcp
Two things that are the same thing: a call does not need a wire, only an
address, and a protocol layer belongs in the module that names it.

Ask names an app and reaches it over that app's canonical socket. When the app
is THIS process that socket is a loop -- the value the caller holds is
serialized, handed to the kernel, read back, parsed into a fresh copy, and
dispatched to a function pointer that was in memory the whole time. Nothing
about the call needed a wire; only the ADDRESSING did, and addressing is what a
by-name call already has. So a bound listener records which App answers for it,
Serving reads that back, and Here runs the op through the op's OWN invoke seam --
the same validate -> authorize -> run core a REST request, an MCP tools/call, a
CLI command and a socket call all land on. One fewer transport, not one more.

That is what a co-resident caller uses INSTEAD of building a request for its own
router to take apart again. The pattern it replaces cost hanzoai/cloud a
re-entrancy counter keyed on a goroutine id parsed out of runtime.Stack, because
dispatching a request into the whole app re-ran every edge middleware and a
middleware that read the app it was serving recursed without bound. A direct op
call cannot express that shape: it enters one op, not one router.

MCP now comes from github.com/zap-proto/mcp v1.0.5 rather than being hand-rolled
here, so it sits beside zap-proto/http as a protocol layer over ZAP instead of
inside the framework as a special case.

TestServePathAllocsAreChainDepthInvariant fails at 34 allocs/request against a
budget of 1. It fails identically at v1.26.0 -- verified in a clean worktree at
that tag -- so it is pre-existing and not this change. Named here rather than
left for the next person to rediscover.
2026-08-05 21:29:52 -07:00
zeekayandHanzo Dev 090bac90f1 zip.Mount is a proxy, not a mount — the name now says which of the two it is
`Mount` meant two unrelated things across the fleet at once:

  zip/remote.go    func Mount(prefix, addr string, decl ...Declaration) (*App, error)
  112 subsystems   func Mount(app cloud.Router, deps cloud.Deps) error

The second is the fleet's registration contract, stated in hanzoai/cloud's
package doc and checked by cloud.MountFunc. The first is a leftover: Mount was
one of five composition verbs (Listen/Mount/Add/Graft/Use), the other four were
dissolved into Use, and this one survived by being the only one that pointed at
another process. A reader seeing `Mount` in a diff could not tell which was
meant without counting arguments.

Renamed to Proxy, which is what it BUILDS. Every route it registers runs the
one handler this file calls `proxy`, whose whole body is forward() to the
address given; "proxy" is the term of art for a local stand-in whose handlers
hand the request to someone else, so it needs no gloss. It is also a noun,
which a function returning a value should be — composition is Use's job now,
and the doc has said so since the four verbs were dissolved.

NOT Remote: [Remote] already exists here and is the CALLER's side, a client
executing commands against a service elsewhere. This is the SERVER's side, a
definition standing in for that service inside this program's routing table.
Two sides of one wire are two things.

Also renamed the tests that named it, and mount_test.go -> adapt_net_http_test.go
with it: those pin AdaptNetHTTP, a THIRD sense of "mount", and TestMount_*
named none of the three.

Regression set empty: TestServePathAllocsAreChainDepthInvariant fails
identically on the parent commit (an allocation bound, untouched here).

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 19:30:42 -07:00
zeekayandHanzo Dev cf68b751f3 Shadow is removed: a braided program is unbraided, not accommodated
v1.24.2 shipped two fixes. The operation-id one stays and is untouched here: a
declared WithOperationID survives composition verbatim (measured on hanzoai/o11y
— 353 ops, 217 declare an id, all 217 were being renamed by a host prefix and
now are not). The address one, App.Shadow(), is removed.

Shadow existed to make an op declarable in one place and answerable in another:
a service installing its published table of ops onto the router its
implementation already serves on, a test registering a second handler at an
address deliberately. Both are the same shape — a declaration and the handler
that satisfies it, written apart, with a framework verb holding the two ends
together. That is a braid, and a framework that grows a verb per braid grows one
verb per caller. The fix belongs in the caller: a typed registration is ALREADY
both halves, the handler the router installs and the op the document, MCP tool
list, CLI and SDK all read, so the declaration and the answer can simply be the
same line. Use composes definitions, Group scopes them, and between them every
shape Shadow was written for is expressible with one claim per address.

So the address check goes back to what it was, exactly: one registration at an
address, both parties named when there are two, no scope and no option that can
talk it out of an accident. addrClaim, addressErrors and addressRules are gone
with it — 190 lines of walk.go that existed only to reason about the SHAPE of
multiple claims at one address, a question the language no longer asks.

Removed: App.Shadow, OpScope.Shadow, route.shadow, App.shadow, wrapRouter.shadow
and yields, App.method's shadow parameter. wrapChain and OpScope lose their
nil-wrap branches, which existed only for a Shadow-only scope with no middleware
to compose. No caller outside this repo referenced any of it at a released tag.

Tests: shadow_test.go deleted (250 lines, ten cases, all about the removed
verb). oneclaim_test.go replaces it with three that pin the rule from both
sides — the braid is refused with both parties named, one registration declares
AND answers, and Use+Group place a definition's ops wherever a host wants them
with one claim per address. Everything else is unchanged, including all of
declaredid_test.go and the unnamedApp/billingApp split in compose_test.go, which
are the id fix and not this one.

319 root-package tests pass on a clean testcache and under -race.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 09:21:25 -07:00
zeekayandHanzo Dev 0ce276b638 a declared id is not composition's to edit, and a yielded address has a spelling
Two defects, one theme: a declaration the programmer WROTE is not something the
shape around it gets to overrule.

occurrenceID qualified every operation id with its occurrence's prefix,
including one the author had written with WithOperationID. Including
github.com/hanzoai/o11y under /v1/o11y therefore renamed its published ops to
v1.o11y.<id> — every MCP tool an agent had cached, every operationId, every CLI
command, every generated SDK method — as a silent side effect of one wiring
line. LLM.md's own Graft table has said `operationIds | untouched` since v1.18;
the walk had stopped obeying it. Measured on o11y: 217 of its 353 ops carry a
declared id, and all 217 were renamed by a host prefix. They are not now.

The rule asks one question first. A DECLARED id survives verbatim, because it is
a published name and a service embedded at a prefix publishes the same
operations it publishes standalone. An UNDECLARED one is still qualified by its
occurrence's prefix, deterministically and from the composition's shape, because
there no promise exists and a real ambiguity does: one definition included twice
is two operations and a document cannot hold two under one id. Where the two
meet — declared AND twice — is now a refused program that names both occurrences
and both fixes, instead of a silent rename that resolved an ambiguity the author
never saw.

Two registrations at one address was a refused program, and that refusal is
right. It also refused two shapes the estate writes on purpose: a service
installing its own published TABLE of ops onto the router its implementation
already answers on (o11y: 353 ops, publish() after the service's own routes —
"order is the contract"), and a second handler at an address declared to pin
what the router does with two (cloud's openapi generator test). Nothing in the
program said which it was, so the check could not tell them from an accident.

App.Shadow() says it. Scoped like With, and for the same reason — a group is an
App, so app.Shadow().Group(p) carries the property onto the group and every leaf
beneath it inherits it, including leaves registered later. Nothing about the
wire, the registry or any projection changes: the route installs exactly as
before and fiber chains it behind whatever came first. Only the conflict check
changes, and into two NARROWER rules rather than none —

  - exactly one claim at an address may decline to yield, and it must be the
    FIRST, because the router answers with the first registration. A shadow
    ahead of what it yields to would stand in front of it, so "order is the
    contract" is now checked rather than stated. Two claims that both mean to
    answer is still the accidental collision, refused in the same words.
  - at most one claim may carry an op. A document keys an operation on method
    and path; there is one slot, and two ops asking for it would publish one and
    drop the other. Shadow yields an address to another HANDLER, never a
    document slot to another op.

A shadow that is the only claim simply answers, which is what lets ONE table be
mounted into a host that has an implementation and into one that does not.

Shadow is on the concrete *App and deliberately NOT on the Router interface:
Router is implemented from outside this package method by method (cloud's scope,
commerce's mintRouter), and widening it is what stalled v1.19 adoption once.

Tests: declaredid_test.go and shadow_test.go, twelve cases, each failing before
the change — the id one with the literal v1.o11y.CreateRole, the address ones
with the refusal origin/main emits. compose_test.go gains unnamedApp, the same
definition with no declared id, because a definition that names its ops cannot
be the one included twice: the diamond is a test about the UNDECLARED half of
the rule and now says so.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 02:22:55 -07:00
zeekayandHanzo Dev 08da1b6858 an operation can state its request facts and its success codes
Bridge exists in cloud because zip lacked two vocabularies. Both close here, and
both close the same way: the fact rides the op's CONTRACT, so every projection
sees it. A replacement that were equally invisible would not be one.

Gap 1 — a header tag. `Tenant string \`header:"X-Tenant"\`` binds off the wire
with no middleware anywhere, and appears as a header parameter in the document,
a flag on the command, and a property in the tool schema. Binding order is body,
then headers, then URL: the URL is the addressing authority. On a transport with
no HTTP request — the CLI, and the call plane — the answer is nothing rather
than a nil panic, which is the shape CallerOf already established.

Gap 2 — WithStatus takes a SET, and the answer states which one it is via
StatusCoder. The status is a property of the value the handler already returns,
so there is no mutable slot: a slot is a side channel and a side channel is what
Bridge was. Every declared code reaches the document, so a client has a branch
for each. A code the op did not declare is refused as an error — not a panic,
because it is a runtime value on a serving goroutine.

Also: App.Routes and App.Test now exist. Router's comment recommended both while
neither did, and they are the two things callers reached through Fiber() for —
so the stale reference is fixed by making it true rather than by deleting it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 19:43:13 -07:00
zeekayandHanzo Dev 3407863fb5 the walk is a visitor; the occurrence type is gone
walk(app, visit func(n node, ctx scope, site callsite) error). The callback's
parameters WERE the payload, so the struct wrapping them was a noun invented to
carry what three arguments already carry — and retaining the slice made every
backend a consumer of a materialised intermediate rather than a reducer over a
traversal. The English word survives in prose because the definition/occurrence
DISTINCTION is load-bearing; the type does not.

Validation stays its own first pass (verify), so cycles and conflicts complete
with trails before any backend sees a visit. A generation retains what was
REDUCED — router, ops, host set — never the traversal.

Also: #4 verified, not changed. Own-op tool names already ride the operation-id
rule because they read Registry(); a plugin catalogue tool is invoked by NAME at
the plugin's own door and never consults a prefix, so ONE definition is ONE tool
and namespacing it would invent a callable that does not exist. Tests pin both.

#2: Drop and Include share one transact path, so the guarantee is identical, and
the lock discipline that makes it deadlock-free is now written down with the two
invariants it rests on. Documented honestly: commitment across servers, not
simultaneity.

Godoc: every bracket reference to a deleted symbol rewritten. LLM.md carries the
canonical three-document spec.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 12:55:22 -07:00
zeekayandHanzo Dev 7e16e8f0af red: 9 of 12 adversarial findings fixed, 3 are design calls
Fixed, in severity order:
- transact's rollback is DEFERRED and now restores adopted teardown hooks too,
  so a panic in the build no longer unwinds past it and poisons a.entries. An
  unknown HTTP method in a Declaration is refused at the boundary it crosses
  instead of panicking inside fiber's register().
- App.Group inherits a.wrap. A scoped With used to be lost by a nested group,
  which reopened a gate origin/main held shut — a regression this branch
  introduced when Router.Group changed its return type.
- With(...).Use(definition) is refused loudly. It cannot wrap a definition's
  own leaves, and silently composing them UNGATED was the one unacceptable
  answer for the chain people reach for With to install.
- conflicts() keys on path then method, upper-cases the method, and treats ALL
  as colliding with any method at that path. Deleting App.claim was justified
  on the walk answering better; the ledger's strongest case was mounts and
  plugin prefixes, which register as ALL, so this had to be true before that
  sentence was.
- project() refuses to write a document for a program that does not build.
   used to emit {"routes":[]} and exit 0 for a broken service.
- Drop bumps the version counter; a dropped route could otherwise serve forever
  out of a stale draft.
-  is atomic. Include is by design called against a running system,
  so the check that catches a concurrent Use must not itself be a race.
- A nil handler panics at registration, as router.go always promised.

Still open, all needing a decision rather than a patch: accessors return an
empty projection for a program that does not build (Registry/Declaration cannot
return an error); Include on a non-root is a silent no-op though the freeze
panic recommends it; a middleware-only definition is inert because depth>0
middleware has no routes to compose into.

MCP catalogue migration still outstanding — see f26b559.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 12:34:35 -07:00
zeekayandHanzo Dev 7ec13236ff one verb: Graft, Add, Service and the Mount VERB are gone
Everything composes through Use. A unit of functionality is an *App — whether
it is linked in, loaded from a binary, or another process — so Load and Mount
become definition CONSTRUCTORS returning (*App, error) instead of verbs.

App.claim/release and the claims ledger go too: the walk's conflict accumulator
already answers 'who else wants this address', and answers it better — every
collision at once, with both claimants and their call sites named. Two
mechanisms for one question are two answers waiting to disagree.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 12:34:35 -07:00
zeekayandHanzo Dev 7dc45e8b75 compose: one verb, because the eager registry was the disease
An App becomes a PROGRAM: an ordered list of entries, each carrying one of
exactly three payloads — middleware, a route, or another App INCLUDED BY
REFERENCE. One walk flattens it into occurrences in preorder; every projection
is a reducer over that slice. Graft, Mount and Add delegate to the same walk.

Five verbs existed because the op registry was a FIELD that composition
appended to at call time, so composing two apps meant merging two registries
and AdaptNetHTTP could destroy five projections by type-erasing an *App. A
projection over a walk needs no merge rule.

Prototype only: not for main, not tagged, not released.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 12:34:35 -07:00