main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e6ae1244a7 |
describe: every operation the fleet publishes now says what it does
1465 published operations, 797 described. The other 668 offered an operationId and
nothing else — a generated SDK method with no docstring, a spec-derived CLI command
with no help text, an MCP tool an agent cannot choose between. Now 1491 of 1491.
The gap was structural, not neglect. Almost every one of them was an UNTYPED route:
a proxy to a vendored module, an SSE stream, a WebSocket upgrade, a byte upload, an
All() wildcard, or a surface owned by another repo entirely. None has a handler doc
comment in this tree for zipdoc to lift, which is exactly why 47 apps carried no
zipdoc directive — adding one would have produced an empty file. The seam they
needed existed and had one caller; it now has 523.
Three surfaces had no seam at all and would have been left behind:
- metrics and licensing are vendored modules that deliberately do not import
cloud, so their prose lands at cloud's OWN wire fact in build.go;
- authz is a leaf forbidden from importing cloud, and its handlers are untyped
closures in another module — both seams shut — so its prose lands in
plugin/authz/main.go, the file whose own doc says it is where "cloud's plugin
contract bends to the leaf."
Every sentence was read off the handler, and reading 668 handlers is most of what
this cost. It found ten defects, filed as #376 — two of them money: gpu-charge is
not idempotent, and the finance ledger's peer path emits a vocabulary its reader
does not classify, so credits render empty and deposits sign negative, with the test
green on both paths because it only exercises the S2S mock. None is fixed here.
Describing is not repairing, and a description that flattered the code would have
been worth less than the silence it replaced — so where a route is broken, the prose
says what it actually does.
Three tests used "has prose" as a proxy for "is a typed op". That equivalence held
while prose could only arrive by lifting a typed op's comment, and Describe breaks
it by design — so each of those tests forbade precisely what the seam exists to do.
They now read zip's own registry and assert something stronger: every operation is
either a typed op with lifted prose or a recorded raw address with declared prose,
and either way it carries prose. apps/exec's is a gate over all 56 of its ops, which
matters most on a pure-proxy surface, where the description IS the product surface.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
4cfc10d095 |
docs: the first sentence of a product's doc is the customer's sentence, not ours
The package doc's opening line is no longer read only by us. openapi/synopsis.go lifts it verbatim into the app's OpenAPI info.description, the weave lands it on the product tag, and from there it is the MCP tool prose an agent reads and the line `hanzo <product> --help` prints. It is the first thing a paying customer sees about a product they are deciding whether to use. Forty of them opened by describing the implementation instead: "mounts the Hanzo Cloud /v1/code/* surface", "folds hanzoai/esign (the Documenso fork) into the unified hanzoai/cloud binary as an in-process subsystem (HIP-0106, task #100, epic #96)". A customer reading the tag list learned our file layout, our epic numbers and our mount order — and not one thing about what they get. Several ran past 200 characters, so the CLI help column truncated mid-clause. So sentence one now answers the only question a buyer is asking: code → search and symbols across your repos, for you and your agents. esign → a document out for signature, signed and filed with an audit trail. world → a live news feed filtered to what your project cares about. NOTHING IS DELETED. Every clause the old opener carried — the mount path, the fork it wraps, the HIP, the tenancy argument — moves down to sentence two, where it is read by the person it was written for. Only the ORDER changed: what the product is, then how it is built. Both were always in the same comment; only one of them projects. The rule the forty now hold, and the reason each is a rule: ≤90 characters (the CLI help column); no "mounts"/"surface"/"subsystem"/"plane"/"binary" (words for where the code lives, not what it does); no /v1 path, file name or HIP number (an identifier no customer can look up); no shouting (the tag list is prose, not a changelog). Sentence two keeps all of it. Two files were already gofmt-dirty on main and are now clean, since they are files this touches: apps/content/doctypes.go (comment alignment) and apps/websearch/websearch.go (list-continuation indent). Regenerated FROM SOURCE, not hand-edited: make describe → 1039 paths, 906 MCP tools. The artifact diff is descriptions only — no path, operation, schema or tool name moves. Co-authored-by: Hanzo Dev <dev@hanzo.ai> |
||
|
|
c0d7b3f969 |
openapi: a product tag says what the product is, in its owning package's words
The document has always known a product's NAME mechanically — the first path segment after /v1/ — and never what the product IS. A caller reading the tag list, an agent reading the MCP door, a CLI printing `hanzo <product> --help` got 144 bare nouns. There is exactly one place that sentence is already written and already reviewed: the package doc of the package that implements the app. So this reads it rather than asking anyone to write it twice. openapi/synopsis.go Synopsis(plugin/<app>) -> the owning package's synopsis. describe.go stamps it into that app's own subset as info.description. openapi/weave.go lifts the tag prose off the subsets it already reads. ONE computation, at the one moment an app describes itself. The weave does not look the mapping up a second time in a second process — it reads the value the app that knows it already wrote down, which is why Weave stays a pure function of its parts. The owner comes from the app's own composition root: plugin/<app>/main.go imports exactly the package it mounts. Nothing else could be the source — four apps are not named after their package (audit->auditlog, evals->eval, plugins->plugin, zero-trust->zt) and one package backs two apps (account, account-bridge), so a name-derived guess is right 107 times and silently wrong 5. An app whose subsystem is another MODULE imports no package here and gets nothing, which is the honest answer. And the comment taken is the one that OPENS "Package …", not go/doc's first-file-in-filename-order fallback. Packages that open their alphabetically-first file with a note about that FILE and state the real package doc in <name>.go would otherwise publish "actions.go — the two GitOps write actions" as the deploy product's description. A misfiled sentence reads exactly like a real one; an absent one does not. 109 of 112 apps have a package doc; 85 of the 144 product tags gain a description. The three without are metrics, authz and licensing, whose subsystem is another module — there is no package here to read. The tag NAME is never conditional on a description: the list stays a function of the document's operations, so nothing enumerating products loses a product because nobody wrote a sentence. The fleet identity remains the fallback for a subset whose package has no doc, and the weave treats a part carrying it as having said nothing. THE LIFTED PROSE LOSES THE HANDLER'S OWN NAME, which is the other half of the same problem. A Go doc comment must open with the identifier it documents, and that identifier is Go's, not the document's: "GetSQL returns one database" reached the OpenAPI description, its summary, the MCP tool description an agent reads, and the CLI help line — naming a function no caller can see. zip drops an exact leading match of the handler's own name from v1.18.13 (main is on v1.18.14, whose lift is byte-identical), and nothing had regenerated against it: 35 packages carried prose the pinned zip can no longer produce. They regenerate here. Three test assertions quoted the leaked identifier and now quote the projection. Every generated artifact is regenerated FROM SOURCE (make -f mk/fleet.mk surface-check, green: 1017 paths). Nothing this commit does moves the wire: of openapi.yaml's 16,439 non-prose leaf facts, 0 changed. The 4 lost and 94 gained are all one thing — surface main already decided and never republished: /v1/insights/e removed and /v1/event given its declared body ( |
||
|
|
16576f8169 |
type: 27 addresses that published nothing — 19 now describe themselves
The work list was the ARTIFACT, not a grep: every operation in
plugin/{usage,venue,world,agent,bot,help}/openapi.json carrying neither a
description nor a summary — exactly the set that projects to nothing at all. No
prose, no MCP tool, no CLI command, no typed SDK method. It was 27 of 27.
19 are typed ops now: usage 5/5, venue 5/5, help 4/4, world 4 of 5, bot 1 of 4.
Every one carries a doc comment that is TRUE of its handler, and every published
schema property carries its own — 0 bare properties across all five subsets.
Eight stay untyped and each names its wire fact AT its registration, gated by
untypedByDesign + TestEveryRouteIsTypedOrNamed so the two ledgers must sum to
what the live router serves:
GET /v1/world/stream Server-Sent Events; no Out expresses a stream.
GET /v1/bot/connect a WebSocket upgrade; 101 then duplex frames.
POST /v1/bot/nodes/{id}/invoke a 403 carrying a DOMAIN body a client switches
on, plus the caller's X-Device-Id, which no In
field may carry.
POST /v1/bot/peer/invoke a net/http machine hop with text/plain refusals
and a MaxBytesReader cap.
the four /v1/agent ops registered by github.com/hanzoai/agent v0.1.3
(agent.go:166-169), not by cloud. They become
typeable upstream, which additionally needs a
per-request bridge there; POST /v1/agent also
relays an upstream 4xx's status AND body.
Two latent defects found by typing and fixed:
plugin/venue/main.go declared no Prefixes, so the standalone binary's scope
owned only the /v1/<name> default — and venue is named "venue" and serves
/v1/cloud, so it owned NOTHING it registers. cloud.Declare attributed every
route to no subsystem and scope.Use installed the app's middleware where no
route lives, which is load-bearing now that cloud.Bridge parks the org a typed
op reads. The apps/plan defect, one app over.
apps/bot had no Makefile, so `make -C apps/bot openapi` could not run and that
subset could never be regenerated by the per-app chain — despite mk/plugin.mk
claiming an app cannot have a main and no Makefile. catalog, crawl, meet and
zen are still missing theirs.
One delta, MEASURED rather than glossed and not fixable in cloud: encoding/json
validates the whole document before invoking any custom Unmarshaler, so the
record-it-and-judge-it-later input that preserves gate order for an oversized or
wrong-shaped body cannot preserve it for bytes that are not JSON at all — zip
refuses those first. TestSyntacticallyInvalidJSONIs400Early (help) and
TestSyntaxErrorIs400BeforeTheAdminGate (venue) pin exactly what moved.
Wire preserved otherwise, and pinned: the 413-after-404-after-503 intake order,
the body-tolerant sync, the ?project cross-check that a body cannot become, the
no-store header on the two money reads, the one-or-many usage report, and the
tenant that comes from the validated principal in every case.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
2c4b045b0b |
cmd/cloud + plugin/<app>: the light host is the one binary — scope credentials, forward flags, own "/"
Restructure to the canonical layout: cmd/host → cmd/cloud (the host IS the one real binary; name it cloud), and every other cmd/<app> → plugin/<app>. `ls cmd/` is `cloud/` alone; `ls plugin/` is the 116 per-app + tool dirs. gen-app-cmds scaffolds into plugin/<app> and scans plugin/ for the bijection; the Dockerfile / Makefile / mk / hanzo.yml / weave / controlplane-containment gate all read the new paths. go build ./cmd/cloud links ~399 pkgs and zero subsystems. credz KMS-key leak (#51 follow-up): zip builds each child's env as append(os.Environ(), Plugin.Env...), so a host that keeps CLOUD_KMS_MASTER_KEY_REF hands the root key to EVERY child — the Root posture credz exists to prevent, and now the default entrypoint. cmd/cloud (stdlib credz/launch only — importing credz would drag cek→sqlite and re-fatten the host) mints the launch secret, scrubs the root key from its OWN environment, stamps each child a scoped CREDZ_TOKEN, and re-injects the root key onto the kms broker child's Env ALONE. Every generic child comes up with a token and no key and must ask the broker. Pinned by cmd/cloud/main_test.go and proven by a live dns spawn. helm flag forwarding: cmd/cloud accepts --brand/--domain/--data-dir/--iam-issuer (the args the chart passes the entrypoint) and republishes each non-empty one as its CLOUD_* env, which the per-app children read; an empty flag never clobbers a value already pinned in the environment. console at "/": nothing served the host root once mountConsole moved into the per-app cloud.Serve. Extract the console into a light webui leaf (stdlib + embed + a new strings-only brand leaf, both aliased back into package cloud so no call site changes) so cmd/cloud — the front door — owns "/" and serves the white-labelled SPA. The host stays ~399 packages and imports zero subsystems. Co-authored-by: Hanzo Dev <dev@hanzo.ai> |