zap reads text zero-copy over the frame that arrived, and Call releases that
frame to the pool when it returns, so a reply's strings pointed into memory the
next call overwrites. The generated codecs already copy text for this reason;
the reflective decoder now does the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
forwardIdentity wrote the inbound request's headers, or the stated caller,
and never read the caller ActingAs derived, so the next hop saw the original
org, the original project and no X-Acted-By. The derived caller now wins, as it
does in CallerOf, and is written whole: the org acted for, the actor, the
principal and its billing-account claim, and no project. Trace context still
comes from the request.
Co-authored-by: Hanzo Dev <[email protected]>
X-Billing-Account-Id joins the identity set Call forwards, and Caller.Account
carries it: headers() writes it and CallerOf reads it, the same as every other
identity field. A callee that resolves who pays now reads the account the
gateway minted, over a call as over REST. A credential with no claim sends no
header.
Co-authored-by: Hanzo Dev <[email protected]>
schemaOf fell through to {"type":"object"} for an interface, and the
projector did the same for the `any` prim, so a field holding a token
string or an array was described as an object. The published examples that
carried one failed against their own schema — Hanzo's auto.runIn.auth and
tool.toolCall.arguments — and every generated client typed the field wrong.
An interface is now the unconstrained schema a silent marshaler already gets.
Co-authored-by: Hanzo Dev <[email protected]>
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]>
routerPrefix promises that an unresolvable router "is an error naming the
call, never an assumed empty prefix", and then returned the empty prefix for
a group. So a group handed to another function filed its ops at the path
after the prefix — an address nothing serves — in the document and the MCP
tool alike, which is the silent drop the resolution exists to end.
The one shape this hits is a group reached through a parameter: the prefix
belongs to the call that made the group, and the call is in another
function. The error says so now, rather than leaving the reader to infer it
from a path that looks plausible.
Co-authored-by: Hanzo Dev <[email protected]>
cmd/zipdoc files every entry under the package that declared it, raw routes
included. A raw route records no registrar — a method, a path and a handler
chain is all App.raw keeps — so an address is all anyone can ask by, and the
address lookup could not reach a qualified key. A service's whole untyped
half therefore documented as silent the moment its generated file was
rewritten: 57 of iam's routes at once.
So the keys are indexed by address as they are recorded, and only the
unqualified lookup reads that index. A caller that names a package asked
about that package, and answering it with another's sentence would document
one service with another's words.
An address two packages describe differently is dropped from the index
rather than resolved. One address answers once in one process, so two
claims on it are a conflict, and choosing either would be a guess; the
qualified lookup still reaches both.
Co-authored-by: Hanzo Dev <[email protected]>
A host that re-exports the type writes `type Router = *zip.App` and hands a
Router to every plugin. That is the same type, but it arrives as
types.Alias, so matching the shape directly saw neither an App nor a Group
and the op's prefix could not be resolved — which refuses the whole
document rather than filing one path wrongly. Unalias at each place a type
is matched by shape, including the In and Out inferred from a handler.
A group is also read when var names it, not only :=. The prefix belongs to
the Group call; the syntax that binds the result does not change it.
Co-authored-by: Hanzo Dev <[email protected]>
Cloud composes each plugin as its own definition and asks Declaration for
its routes before composing it. A projection of a program that does not
compose panics rather than lying, so a plugin whose middleware guards no
routes took the host down with it; Build answers instead, but installs a
generation and freezes the definition the host is about to compose by
reference.
Verify is Build's first half and the same call — walk, structural rules,
derived rules, rule adoption — and stops there. The host reports which
group is wrong and carries on.
It answers for the definition as the root of its own walk. Middleware at a
served root is exempt, because composition brings it routes later; one
level down the same middleware guards a settled subtree. Only the host
knows which a definition turned out to be, so both verdicts are right and
the tests say so.
Co-authored-by: Hanzo Dev <[email protected]>
v1.37.9 fixed the REST path and the three by-name seams that already looked the
op up in the composed registry. Two did not: the graph resolved its ops from
that same registry and then invoked them without saying so, and the in-process
invoke did the same. So the contract fell back to the declaration — for anything
declared on a group, the bare leaf — and a rule reading op.Path governed the
REST call while missing the graph call to the SAME operation.
A test now asks both seams about one op and requires the same answer.
Co-authored-by: Hanzo Dev <[email protected]>
The authorizer and the result hook were given the op as DECLARED: for anything
declared on a group, the bare leaf and an id derived from it. So a rule keyed on
op.Path governed an address nobody serves — /probe/:id where the route table,
the registry and the document all say /v1/crm/probe/:id — and one definition
mounted at two prefixes reported a single name for two operations.
They now see what the route matched: the composed pattern, and the composed id
by the same rule the registry uses, where a declared id is the operation's own
name and composition does not get a vote. The concrete address stays off Op,
because path parameters are input and the hook is promised the operation and the
outcome and never the input; a handler that needs the resolved address asks
AddressOf.
Co-authored-by: Hanzo Dev <[email protected]>
Alias existed for one reason, stated in its own doc: a registration made inside
a helper is invisible to cmd/zipdoc, so a service that rolled its own alias
helper lost the prose for both addresses. That is no longer true — the pass
reads Raw on a route table directly, and a fixture now proves two plain Raw
calls each carry the handler's sentence.
So the abstraction was working around a limitation that no longer exists, and
nothing in the estate called it. It goes, with the thirty-odd lines of extractor
special case that recognised it.
Co-authored-by: Hanzo Dev <[email protected]>
The address is a URL path, so a value substituted into it has to be escaped as a
single segment. A trace id of t1/2 went in raw and read as two, so a service
that forwards what AddressOf answers asked its runtime for a route nobody
registered: the request arrived at the right operation and the forward went
somewhere else. A space did the same thing more quietly.
Co-authored-by: Hanzo Dev <[email protected]>
A pattern never ends in "/" now, the root aside, because the router serves "/x"
and "/x/" alike — so two spellings were two keys for one address, and a group
could not name its own root at all: declaring "" on it produced the prefix with
a trailing slash. That is why a collection root had to be declared at an
absolute path on the parent instead, which put the route outside the middleware
of the group it belonged to.
The rule was also written twice. The router composed a prefix with a leaf and
cmd/zipdoc composed the same two things to find that route's prose, so trimming
in the router alone would have filed every collection root's sentence under an
address the document does not contain — complete document, missing description,
no error anywhere. Both copies are gone; internal/addr is the one that spells it.
A test registers a trailing-slash pattern — the shape the Sentry-compatible
ingest addresses use on purpose — and asserts the route table, the registry, the
document's template, and the operation and concrete address a handler reads are
all the same string, because a service that resolves its runtime handler by name
from that pattern refuses every call if two of them disagree.
Co-authored-by: Hanzo Dev <[email protected]>
The pattern says which operation answered; the address says which resource it
was asked about. A handler that forwards needs both, and could only get the
second by being wrapped in a closure that held the decoded input — which is what
put a service's whole surface behind a helper this pass cannot read.
The one seam every projection funnels through holds the resolved pattern and the
decoded input together, so the address is stated once and correctly for REST, a
tools/call, the call plane and an in-process invoke alike, where three of those
have no request to read a path from. It is computed on demand: an op whose
pattern carries no parameter pays a string compare, one that does pays
reflection, and only if something asks.
Deliberately not a field on Op. Op is what a rule is handed and what the result
hook is told, and both are narrow on purpose — the hook is given the operation
and the outcome, never the input. A concrete address carries path parameters,
which are input, so putting it there would widen two contracts to serve a third.
A test pins what those two see, including that it is still the declared leaf
rather than the served pattern: that gap is older than this change and is its
own decision to make.
No additional allocation over the previous patch, measured on
Benchmark_TypedRoute: 63 either way.
Co-authored-by: Hanzo Dev <[email protected]>
The address an op answers at is a property of the CALL, not of the declaration:
a group's prefix composes at build, and a definition composed under two hosts
has two addresses and one handler. So a handler could not learn its own address,
and a service that needed one composed it in a wrapper and threaded it in —
which made every one of those registrations a call this pass cannot see, and
took a whole service's operations out of the document, the tool list and the
SDKs.
OpOf reads it from the context instead, so the declaration stays the bound
method the projections resolve. The seam that knows the occurrence states it:
the REST route that matched, and the by-name seams, which looked the op up in
the composed registry. Under that, the one contract every projection funnels
through states the declaration, so a handler always has an operation to read and
a direct call says there is none rather than guessing.
Costs two allocations on a typed REST request, measured: 61 to 63 on
Benchmark_TypedRoute, with bytes unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
The wire is stated in three places that have to agree: the option set, the
constant's value, and the comment explaining it. The existing cases fail the
moment the options move — and would pass happily while the name went on
claiming semantics the encoder no longer has, which is the one an operator
reads in a startup line.
So the name is now asserted against the bytes: it may say "sorted keys" exactly
when a map comes out sorted, and "v1 semantics" exactly when a zero under
omitempty is left out. It fails in both directions — options moved, or name
moved — and says which disagreed.
Co-authored-by: Hanzo Dev <[email protected]>
Variant said "encoding/json/v2", which reads as v2's semantics — a zero kept
under omitempty, a nil slice as [], case-sensitive decoding, a map's keys
unsorted. This wire does none of that: the options are v1's, plus sorted keys.
Naming only the engine misleads in exactly the direction that matters, and the
one moment anyone reads it is a startup line in a log.
It is also a declaration rather than a reading of the toolchain. zip marshals
through v2 because this package imports it; GOEXPERIMENT=nojsonv2 does not fall
back to v1, it fails to compile — so the name cannot describe an encoder the
binary is not using.
Co-authored-by: Hanzo Dev <[email protected]>
net/http's own server always fills RequestURI, so a handler is written expecting
it. Converting a fiber request fills URL and leaves that field empty, and the
handlers that read it are the ones that relay — a proxy, an ingest address,
anything that forwards what it was asked for. An empty target normalises to "/",
so a relay asks its runtime for a path nobody named.
The test reads the field rather than the path, because http.ServeMux matches on
URL.Path and is green either way.
Co-authored-by: Hanzo Dev <[email protected]>
The authorizer was already per-definition and a group has always been one, so a
group could hold a tighter rule than whatever composed it — App.rule asks its
own first. What was missing was the way to say so: Authorize existed only on
App, and a subsystem that guards its own writes had to assert its group back to
the concrete type to reach the field.
Without it there are two spellings and both are wrong. The rule on nothing
leaves every write there authenticated and unchecked. The rule on the host makes
it the HOST's, and that covers every op the host serves — so a sibling
subsystem's ops answer to rules written about a principal this guard never
attached, and every valid call to it is refused.
Co-authored-by: Hanzo Dev <[email protected]>
The variant was a literal in the test and a constant in the encoder, and the
two spellings of one fact are what drifted when the encoder moved to v2: the
suite went red over a name, not over any behaviour. The test now asks the
encoder.
Co-authored-by: Hanzo Dev <[email protected]>
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]>
zip called encoding/json, which on Go 1.27 is already the v2 engine with v1's
options — so the speed was never the question. What was at stake is the
semantics: v2 keeps a zero number or false under omitempty, writes a nil slice
as [] and a nil map as {}, leaves `<` unescaped, matches field names
case-sensitively, and does not sort a map's keys. Selecting the engine by build
tag meant a module moving to 1.27 changed what every response said, and this
package had gone back to v1 to stop that.
Now it calls v2 and says what the wire is: v1's semantics, plus sorted keys,
because bytes that vary defeat an ETag, a cache, a signature and a git blob.
Each remaining difference is a decision to make here, once, in view of the
clients — never a default to inherit.
What v2 gives back is its API. Write and Read stream straight to and from the
connection with no second copy, and omitzero says what omitempty could not.
Co-authored-by: Hanzo Dev <[email protected]>
The crash trigger asserted 200, which says the panicking plugin flushed its
answer before the process went. Whether it wins that race is a scheduling
accident; what the test is for is the sentence after it — the host survives and
the plugin comes back.
Co-authored-by: Hanzo Dev <[email protected]>
Both front ends project through the same projector, so a change to what the
document says about refusals reaches the Go side the moment it is written and
the Rust side only when its projection is regenerated. The two disagreed by
172 lines — every default response and the problem-details schema — which is
the parity test doing exactly its job.
Co-authored-by: Hanzo Dev <[email protected]>
A walk over typed structs collapses every miss to the zero value, so a document
missing a whole response reads exactly like one whose $ref is empty, and the
assertion could only ever report "". It now fails at the first absent step and
prints the keys that are there.
Co-authored-by: Hanzo Dev <[email protected]>
fiber's default test deadline is one second, which measures the round trip. A
request to a lazy plugin waits for a CHILD PROCESS, and on a busy machine that
outlasts a second by itself — so six tests failed on a deadline that was never
measuring them, in a different subset every run, which is what made the whole
suite's baseline untrustworthy.
A generous ceiling weakens no assertion: a served request still returns the
moment it is answered.
Co-authored-by: Hanzo Dev <[email protected]>
The callsite is recorded by counting frames out to the programmer's line, and a
verb method on a scope sits one frame further away than the package-level
function does. The count said otherwise, so every conflict, cycle and post-seal
write in a scoped service cited a line somewhere above the registration instead
of the two lines actually in conflict.
Two tests now hold both halves of that attribution — the package an op is filed
under and the line it names — across all three forms a registration takes,
including a handler another package composed, which is the shape that once
published seven ops with no description at all.
Co-authored-by: Hanzo Dev <[email protected]>
The extractor matched one call shape: zip.Get[In,Out](target, path, fn), three
arguments with the target first. A scope's verb methods carry two, and the
receiver is the target, so every route migrated to scope.Post(path, handler)
went silently undocumented — the op still served, and its prose stopped
reaching the document and the tool list.
Both forms now resolve the same handler selector to the same declaration, and a
trailing metadata chain does not hide the call from the walk.
Co-authored-by: Hanzo Dev <[email protected]>
Go 1.27 allows generic methods on concrete types, so a scope's Get/Post/Put/
Patch/Delete infer In and Out from the handler. A declaration carries no type
arguments and reads as what it does:
accounts := app.Scope("/v1/accounts").Tag("accounts")
accounts.Post("/", api.CreateAccount).ID("accounts.create")
Interface methods still may not declare type parameters, which is why these
live on the concrete *Scope rather than on Router. The package-level Post and
its siblings are unchanged and register through the same path, so the document,
the MCP tool, the CLI and the call plane keep reading one registry — the two
forms publish byte-identical documents, which is a test.
The handler is the bound method, passed directly, so the selector survives in
the source and the projections can still resolve a method's doc comment.
Co-authored-by: Hanzo Dev <[email protected]>
zipdoc files an op's prose under the package whose source holds the registration
and the handler's doc comment. zip looked the prose up under the package the
HANDLER was declared in, which is the same answer only while the two coincide.
A handler composed by a helper elsewhere breaks it. hanzoai/cloud registers every
metered op as fare.Paid(s, o.listBuckets) — one decision about money in one place
— and under Go 1.27 that closure reports internal/fare, so apps/s3's seven paid
ops published no description in the document, the MCP tools or any SDK. Go 1.26
reported the registering package for the same code: whether the closure is
inlined is the compiler's business, so the doc key cannot rest on it.
Co-authored-by: Hanzo Dev <[email protected]>
jsonenc chose encoding/json/v2 on the goexperiment.jsonv2 tag, and Go 1.27 sets
that tag by default. Moving a module's go directive to 1.27 therefore changed what
every zip service writes: a zero number or false kept under omitempty, a nil slice
as [] and a nil map as {}, field names matched case-sensitively, map keys unsorted.
hanzoai/cloud went from green to 54 failing wire tests on that bump alone.
jsonenc is stdlib encoding/json and nothing else. Since 1.27 that API runs on the
v2 engine with v1 semantics, so the speed arrives with the toolchain and the bytes
stay put. A test pins the four differences.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo.ai's Slack agent went silent. Messages arrived — 18 event deliveries in
six hours — and not one agent run began. Every POST to the events endpoint
returned 502 after 43 to 64 seconds, and the host log said this, twice, at the
same timestamp:
23:39:31 zip lazy plugin started on first request pid 1017
23:39:31 zip idle plugin evicted reason=idle idle=2h37m58s
Started and evicted in the same second, reporting an idleness from before it
started.
evictIfIdle reads lastUse without the lock, decides, and then calls evict,
which takes the lock and swaps the child out. Its comment argued the window is
harmless: a request landing in it "is already being served by the instance
retire() is about to DRAIN, or it arrives after the swap and starts a fresh
child". There is a third case, and it is the one that happens. A request can
START the plugin inside that window — target() brings a lazy plugin up with a
CAS on p.cur — and the swap then takes down a child that has served nobody.
The caller waits out the drain and gets a 502. load.go already stamps lastUse
on start "or the next sweep evicts what just started"; the stamp lands, and the
sweep that already decided evicts it anyway.
So evict re-reads the clock under the lock it is already holding. lastUse
moving is precisely the signal that someone used or started the plugin since
the decision was taken, which makes the decision stale. Callers pass what they
observed; 0 means no idleness is being claimed.
Co-authored-by: Hanzo Dev <[email protected]>
The package did not build. project_zap.go declared `type idl` while
zapread.go, in the same package, imports github.com/zap-proto/go/idl and
calls idl.Parse — so every reference to the import resolved to the struct:
project_zap.go:65:6: idl already declared through import of package idl
zapread.go:52:2: other declaration of idl
The package name is the half that cannot move, so the type does. `namer`
rather than `naming`, which layout.go already has.
Nothing else changes: same fields, same methods, same behaviour.
Co-authored-by: Hanzo Dev <[email protected]>
Writing the manifest fragments during macro expansion had a failure nobody
would see: rustc reuses a cached expansion, so a macro that did not run left
its file saying what the source used to say. A rebuild after deleting one
fragment recompiled the crate and did not restore it — measured — which means a
service could publish a document that quietly disagreed with its own code, the
one failure this whole design exists to prevent.
So the description is compiled IN. `#[derive(zip::Wire)]` gives a type
`stated()`, its manifest entry as a `&'static str`, and `reach()`, which claims
its entry and then asks the types of its fields — the graph is walked by the
types themselves, because a macro sees only the item it is on. `#[zip::ops]`
joins the ops half to whatever the ops reach and hands back `manifest()`. The
service prints it on `--manifest`, so what is projected is what this build
declared and there is nothing to go stale.
That leaves one shape for a manifest instead of two, so zipc reads a document
and no longer merges a directory of fragments; what it refused about a
directory it now refuses about the document, in Manifest.Check beside the
version rule. The example's build script tolerates the documents not existing
yet, which is the state of the one build that makes them.
A Rust developer writes Rust handlers, with Rust types and Rust doc comments,
and gets every projection from that source — the same deal a Go developer has
had. No .zap is written by hand and no Go program declares a Rust service's
ops.
The split falls on a measured seam. What is native per language is DECLARING an
op and RUNNING it; what is shared is the projection logic, driven from a neutral
description of the registry.
Manifest the registry with the Go taken out: ops, types, fields, wire
names, prose. App.Manifest fills one in by reflection; a
proc-macro fills one in as a Rust crate compiles.
Project* the OpenAPI document, the MCP tool list, the CLI and the ZAP
IDL, read off a Manifest. Go's own three now go through them —
the whole existing suite is the oracle — so Go keeps no private
path for them and the other languages cannot rot.
cmd/zipc the build-time projector. It reads a manifest, or the fragments
a compiler wrote, and writes the four documents.
rust/ zip in Rust: #[zip::ops] on an impl block, #[derive(zip::Wire)]
on a type, a router, a JSON binder, an HTTP door and a ZAP door.
The wire is generated by zapgen from wire.zap; nothing here
spells a byte offset by hand and nothing links Go.
The layout rule moved out of the manifest and into the projector, so the offsets
in a .zap are derived once for all three languages rather than three times;
TestProjectZAPMatchesRegistry pins that derivation against the layout the Go
encoder actually writes.
One service, written twice — corpus/info in Go and rust/examples/info in Rust,
fourteen ops each — is the gate. The OpenAPI document, the MCP tool list and the
CLI command tree come out byte-identical, and the ZAP schema agrees on every
struct, type and offset; it differs only where a field carries the declaring
language's own case convention and where the ledger names a type the way the
person who must change it would grep for it.
The rule was stated as "a Handler may never terminate a request", and that is not
what the check does or what the framework allows. A gate that cannot refuse is not
a gate: an auth check, a rate limiter, a cache hit and a maintenance refusal all
write a response and return without Next, and they always worked.
What structural refuses is narrower and stays: a handler that CLAIMS AN ADDRESS
from Use position — `app.Use(zip.Static(assets))`, which answers every address
beneath it with a file including ones nothing declared. A gate answering a request
that already matched a route adds no address, so no projection can disagree with
the router; a leaf in Use position invents addresses the document has never seen.
Those are two different facts and only one of them is a rule.
The overstatement cost something real. A consumer read it and routed a 503 refusal
through the host's error handler rather than answering directly — and a host that
flattens propagated errors to 500 turns that into the wrong status. So the
behaviour is now a test rather than a sentence.
The README gave no version to install and no service to run, never
mentioned zipdoc, Described, ErrorHandler, the call plane's field-order
rule, zipgen, CommandsFromSpec, Authorize or the MCP door's limits, and
named APIs that do not exist (app.Add, app.Mount, a zip.Load that takes a
prefix first). It now takes one service through REST, MCP, OpenAPI and the
CLI with output from real runs, then covers each projection, the error
format and the release that introduced it, and the security model.
examples/local-service is that service: one SQLite-backed operation, doc
comments run through zipdoc, a .mcp.json for Claude Code, and a test that
checks every operation against every projection. It is a module of its own
so the SQLite driver stays out of zip's graph. CI runs zipdoc -check and
its tests.
LLM.md is now a map of the code, the test commands, the release steps and
the invariants that hold today.
Co-authored-by: Hanzo Dev <[email protected]>
zip's go.mod requires github.com/zap-proto/go v1.8.3, and contract/go.mod
still listed v1.3.0, so `go test ./...` in contract/ stopped at
"updates to go.mod needed" before compiling anything. That is CI's
go-test-collector step. go mod tidy changes only that requirement and its
go.sum lines.
Co-authored-by: Hanzo Dev <[email protected]>
initialize always answered 2026-07-28. The official TypeScript client
(SDK 1.30.0) and Python client (SDK 2.2.0) ask for 2025-11-25 and refuse
an answer in a revision they did not request and do not know, so neither
could connect to /mcp.
The MCP lifecycle's version negotiation answers in the requested revision
when the server supports it. This door serves 2025-06-18 and 2025-11-25 as
written (the same Streamable HTTP transport and tool methods, and no
batches), so those are echoed. Anything else is answered in 2026-07-28, as
before. With the change both clients connect, list the tools and call one,
the Python client in its default auto mode as well.
Co-authored-by: Hanzo Dev <[email protected]>
Every non-2xx a typed op answers is an RFC 9457 problem document, or
RFC 6749's error pair at an OAuth address, and the document declared
neither, so SDK generators typed errors as unknown. Each operation now
carries a default response that points at a problem-details or an
oauth-error schema. The OAuth addresses come from composeOAuth over the
same walk the error handler is built from.
Neither schema name can be a Go type's name. Neither schema writes a
boolean additionalProperties: CommandsFromSpec parses schemas as objects,
and a boolean failed it.
TestTheDocumentDeclaresTheRefusal checks the declaration at both kinds of
address, and that the refusal on the wire carries every member the schema
requires. The ErrorHandler and MarshalJSON comments still described the
old envelope and a written instance member; they describe what is written.
Co-authored-by: Hanzo Dev <[email protected]>
zipdoc files an operation's prose under its package's import path. At run
time pkgOf read the package off the handler's function name, and a built
binary names package main's functions "main.", so the lookup missed: the
document, the tool list and the CLI help carried no prose. Under go test
the package keeps its import path, so every test passed.
pkgOf now reads "main" as the build info path, which is the import path
zipdoc wrote. TestProseReachesAServiceInPackageMain runs zipdoc over a
package main service, builds it and reads the document the binary
writes. It fails without the change.
gofmt realigns one struct field in typed.go, and the WithStatus comment
names the problem document instead of the envelope it replaced.
Co-authored-by: Hanzo Dev <[email protected]>
2f5a387 added origin, rebase and 26ab3688408b38db62d9a92e46eba140d421db23.
Each holds a single Co-authored-by line: the output of a mistyped shell
command, not source.
Co-authored-by: Hanzo Dev <[email protected]>
A codec is a runtime encode/decode step, which is the thing ZAP removes: a
field IS its offset, and a read is a pointer into the bytes that arrived.
Nothing here runs on the served path — Layouts reflects at BUILD time and emits
constant offsets. Naming it for the layer it exists to delete told readers the
opposite of what it does.
LayoutOf already names one type's shape; this emits those as source.
Co-authored-by: Hanzo Dev <[email protected]>
Authorize is ASKED before a handler and may refuse. Nothing is TOLD after one,
which leaves a gap that no middleware can close: everything a middleware records
is recorded about a REQUEST, and an in-process invoke is not one. An operation
reached that way runs with no trace of having run — no trail entry, no metric,
no span — and it is the door an operator on a pod uses.
So the hook sits at the same seam, which is the one place every projection funnels
through, and is told about all of them. A refusal is told about as surely as a
success, because Deny returns through the same error it reads.
It is told the operation and the outcome and NOTHING else. Not the output: a hook
that could see the value would be a way for a response body to reach whatever the
hook writes to, and every recorder in this estate promises it never reads bodies.
Not the input either — Authorize already sees that, before the handler, where a
decision belongs.
Named returns made that possible and cost one bug on the way: binding the
handler's result to the `any` return wraps a nil *Out in a non-nil interface, so
a void op stopped answering 204. opstatus_test.go caught it, the result stays
typed, and the comment says why.
Co-authored-by: Hanzo Dev <[email protected]>
The descriptor-passing change reached for syscall.FcntlInt, which exists in
x/sys/unix and not in Darwin's syscall package, so every build on a Mac has
failed since it landed. The last tag builds; main does not, which is the shape
that gets noticed late.
x/sys/unix is already required, so this adds nothing but the import.
Co-authored-by: Hanzo Dev <[email protected]>
A descriptor is not data: its number is an index into a table the peer does not
share, so a byte stream cannot hand over a pipe, a console, or an already-open
file. SCM_RIGHTS is the kernel doing that translation, and it rides the same
unix connection peerOf already reads SO_PEERCRED from.
Needed to carry a sandbox control plane. gVisor's urpc passes Files on 20 of
its 22 call sites — exec's stdio, checkpoint restore, the memory file — so a
transport without this cannot replace it at all.
Refuses rather than half-delivering: an empty body would let the kernel discard
a control-only message and lose the descriptors while the call reports success,
and a non-unix connection would deliver the bytes without the files.
Co-authored-by: Hanzo Dev <[email protected]>
A plugin host is where a service becomes an arbitrary-code-execution vector:
whatever it starts runs with the host's authority — verifying blocks, signing,
serving. Sum existed but only gated URL, so the case that looked dangerous was
checked and the case that is was not. A binary read off disk is not more
trustworthy than a downloaded one; it is the same bytes with the download
already done by somebody else.
Now every source takes the same path, and the check is worth something because
what runs is what was checked:
- The binary is COPIED into a directory only the host can write, and the copy
is hashed and executed. Verifying in place and then running leaves whoever
can write that file free to change it afterwards, and starting a plugin is
an observable event, so they get to choose when.
- Holding a descriptor to the original would not have closed that, which the
test proves rather than assumes: a rename swaps the inode, but a WRITE keeps
it, and a write is visible through an already-open descriptor. Only the copy
puts the bytes out of reach.
- The copy is executed through its own descriptor (/proc/self/fd on Linux), so
no path is resolved a second time. It is reopened read-only first — Linux
refuses to execute a file anyone holds open for writing.
- A mismatch refuses. A host that logs and carries on has performed the check
and kept the risk.
What this cannot decide is where a trustworthy Sum comes from. A digest in the
same config an attacker just edited proves nothing, so the anchor has to be
something they do not control. For a network that already agrees on things by
construction, that is consensus.