Commit Graph
4 Commits
Author SHA1 Message Date
hanzo-dev 55ef5731e0 docs: the first sentence is the product the customer bought, not the plumbing
A package's first sentence is not prose that stays in the file. openapi.Synopsis
reads it at describe time, describe.go stamps it into that app's own subset as
info.description, the weave lifts it onto the product's OpenAPI tag, and the same
string is the CLI group help line and the MCP door prose. So it is the ONE
sentence a paying customer reads before they know anything else about us — and
across this slice it was answering a question they never asked.

"Package functions mounts the Hanzo Cloud /v1/functions surface" tells a buyer
where our routes live. It does not tell them they can publish code and call it
over HTTP. "Package meet is the CONTROL plane for the virtual office" names our
half of a split they cannot see. "Package o11y is the ONE owner of the cloud
binary's observability plane — ... every part of the concept:" published a colon:
the paragraph had no sentence break, so the whole registry-internals paragraph
WAS the description. Thirty-seven packages led with a mount point, a route
prefix, an implementation noun or an internal ordering argument.

Each now opens with what the customer gets, in their words, and every fact that
was in the old opener is kept verbatim one sentence down — the route prefix, the
tenancy boundary, the fail-closed behaviour, the topology. Nothing is deleted;
it is reordered so the first sentence answers "what is this" and the rest answers
"how does it work". The four packages here with no customer (k8s, s3admin,
datastore, controlplane) keep an internal first sentence, minus the ALL-CAPS and
the word "binary" that made them read like release notes.

openapi.yaml and plugin/*/openapi.json are regenerated from source in the same
commit, because a doc change IS a published-surface change and the drift gate is
right to say so.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 22:52:20 -07:00
hanzo-dev 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 (6fc2d88c), the six
project-scoped git smart-HTTP paths (811ff080), and the sessions' `terminal`
property (afdda829). The three bare-root git paths reach no app, so they join
router_test.go's unreachable ledger, recorded on the first regeneration that
published them.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 21:33:38 -07:00
hanzo-dev bf1ff22763 type: 23 operations that published nothing, 19 of them now say what they do
The six subsets — notify, product, referrals, validators, zero-trust,
blueprint — were at described=0. Every operation carried neither a
description nor a summary, which is exactly the set that projects to
NOTHING: no prose, no MCP tool, no CLI command, no typed SDK method.
Nineteen are typed ops now (notify 1/4, product 4/4, referrals 4/4,
validators 4/4, zero-trust 4/4, blueprint 2/3), each with a doc comment
that says what the route does, per-field prose on every published
property, and a gate in the package's own typed_wire_test.go whose two
ledgers must sum to what the live router serves.

The four refusals are ONE class: two 200 shapes at one address.
notify's three /send routes answer with a bare SendResponse for a single
recipient and {items:[…]} for several; blueprint's /sbom answers a bare
Estimate for ?template= and {data:[…]} for none. An op declares one Out,
so either shape would publish the other as a lie — worse than none,
because every generated SDK binds it. TestSendAnswersTwoShapes measures
that pair rather than asserting it, so the conversion is a test away the
day zip can declare a polymorphic response.

The wire did not move. A refusal that used to precede c.Bind still
precedes the decode, because a typed op runs after it: requireOrgOnWrite
/ requireAdmin are method-scoped gates on the subsystem's own group, so
an anonymous caller with a malformed body is still 403 and the sibling
/health probes stay open. Every body-only field on a converted POST
carries url:"-", because zip binds query OVER the body and would
otherwise mint a higher-authority ?field= twin no route ever read. A
query scalar whose existing parse trims stays a STRING, measured rather
than assumed: fiber percent-decodes a query value but not a path
segment, so an int field would have narrowed ?limit= and ?tokenId=.
Every body that used to marshal a map[string]any is a struct whose
fields are declared in the map's sorted key order, and the byte order is
pinned.

Four latent defects, all surfaced BY typing:

  - apps/zt had no cloud.Bridge on any of its three prefixes. Its own
    harness mounts on a bare app with no Serve, so every typed op there
    would have seen no org and refused a valid request.
  - plugin/{product,zero-trust,referrals}/main.go declared no Prefixes,
    so MountPrefixes' /v1/<Name> default covered nothing product or
    zero-trust serves and half of what referrals does — the apps/plan
    defect, three more times. All three take manifest.PrefixesFor now.
  - 38 published properties (26 referrals, 12 zero-trust) were about to
    reach openapi.yaml, every SDK and every MCP inputSchema bare.
  - zipdoc cannot resolve zip.App.With as a router, so the obvious fix
    for the decode-order problem fails generation. Recorded in LLM.md.

Regenerated: six subsets and the woven openapi.yaml (1013 paths,
unchanged — the diff is prose plus the two _by_id -> _id operationId
renames a route always takes when it goes typed).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 09:58:36 -07:00
hanzo-dev 2c4b045b0b cmd/cloud + plugin/<app>: the light host is the one binary — scope credentials, forward flags, own "/"
Hanzo CI/CD / cicd (push) Successful in 22s
CI/CD / gate (push) Successful in 22s
CI/CD / containment (push) Successful in 1m37s
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>
2026-07-28 12:37:17 -07:00