feat/anon-pointer-position
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
077fce140b |
money: credit is issued by a human, so every automatic path goes
An hourly goroutine in authors deposited credit with nobody in the loop, and a
GET on three surfaces accrued-and-paid on read. Both are gone, along with the
capability that made them one line each.
- apps/authors/scheduler.go, sweepAndPayout, autoPayoutAuthor — the unattended
hourly accrue+pay loop, default ON, no env var, no route, no human.
- the lazy sweep on GET /v1/authors, /v1/affiliates, /v1/affiliates/me and
/me/earnings. Reads read; the admin POST sweep still accrues.
- payout settlement in both programs. A payout RECORDS what is owed, for every
method including credits; a human settles it. Accrual — the product — stays.
- treasury.Reserve/Credit returned backed=true when unmounted, and `mounted` is
a package global, so in one-binary-per-app it was ALWAYS nil in callers: every
"reserve-backed" payout was an unbacked mint that logged itself as reserved.
With settlement gone it has no callers, so it is deleted rather than fixed.
- POST /v1/admin/credits — a second admin mint with no cap and no positivity
check, whose audit did not fail closed. core.ApplyGrant is the one door: it
caps, rejects non-positive amounts, checks the org, and refuses without a
durable audit store. The relay and its wire client are deleted.
- payout.Client.Deposit, the ONE money-in primitive all three programs shared,
and the deposit method on each program's seam. The seams now carry a single
read, matching referrals: reviving a mint has to start by re-declaring the
capability, in front of a test that says no.
- the published POST /finance/starter op, advertising a grant deleted in
|
||
|
|
3b50091c77 |
mcp: the catalogue is a query — delete the 116 committed tool files
The fleet's agent door answered from plugin/<app>/mcp.json: the tool array each
app's binary projected when it was BUILT, embedded by plugin/embed.go and handed
to zip as Plugin.Tools. 116 files, 49,865 lines, and a second source for a fact
every child already knows.
A second source can only be stale or accidentally correct. This one was stale in
the way no gate in this repository could see: o11y's 353 missing ops live in
github.com/hanzoai/o11y, so a go.mod bump in ANOTHER repo invalidated an artifact
in this one with nothing in the diff to say so. Regenerating it more often is not
the fix — a generator on a hook is still two sources with a race between them,
and the trigger is in a different repository. (
|
||
|
|
f403c603f1 |
cloud: the composition root's verb is Listen, because that is what it does
CI/CD / gate (push) Canceled after 0s
CI/CD / containment (push) Canceled after 0s
CI/CD / image (push) Canceled after 0s
CI/CD / rollout (push) Canceled after 0s
CI/CD / reach (push) Canceled after 0s
CI/CD / fanout (push) Canceled after 0s
CI/CD / receipt (push) Canceled after 0s
Serve and Listen were two names for one act. zip's App already calls it Listen — `app.Listen(zapAddr, httpAddr)` — and this function's whole job is to build that app and hand it its addresses, so calling it Serve made the entry point disagree with the thing it enters. One verb, all the way down: a plugin's main says cloud.Listen, cloud says app.Listen, and nothing has to be translated in a reader's head on the way through. 117 composition roots move with it. ServePlane is untouched — it names a different act (bind one app's own socket for the internal plane), and collapsing it into this would be the opposite of the point. Also fixes apps/iam's TestMain, which had gone red on every store test: credz.Boot's last resort is cek.EnsureDevKey, and that DECLINES on a codec-linked build by design — a build that can really encrypt must be handed a real key, not invent one. So the throwaway goes in through the same door a deployment uses, and only when nothing else supplied one. Six failures back to the one pre-existing ratchet (iam serves 97 untyped ops against a budget of 88). Co-authored-by: Hanzo Dev <dev@hanzo.ai> |
||
|
|
34c13d3464 |
zt, gateway: "edge" named a position, so it stops naming a route
/v1/edge/nodes -> /v1/networks/routers.
Four unrelated things wore the word, which is exactly why /v1/edge read like a
missing product: hanzoai/edge (the on-device inference runtime — a binary the
customer runs on their OWN machine), the public catalogue cache, the gateway
policy role, and these — ZT fabric edge-routers. A prefix belongs to a product a
customer calls, so a position word gets none. hanzoai/edge keeps the repo name;
it is the one honest use, because it genuinely runs at the edge of the network,
the users device. /v1/edge now 404s at every depth and that is the right answer.
The routers move UNDER the network because an edge-router IS a node of the
overlay, so the resource lives where its parent does — zt already owned
/v1/networks. The envelope moved with the address ({routers:[...]}, not
{nodes:[...]}): an address and its payload naming one thing two ways is the same
defect one level down. edgeNodeView/edgeNodeList/toEdgeNodeView follow.
"routers" is a literal beside "/:id". MEASURED on this router, not assumed: the
static segment wins over its param sibling in EITHER registration order, so
unlike the /v1/s3 order-118-vs-120 case there is no ordering for a test to
freeze. It still registers first, per routes() own stated rule.
The edge tag is gone from the woven document (149 -> 148 tags) and no longer
inherits zt package sentence.
gateway: the tag sentence customers read said "live control of your API edge",
which spends the word on a position in the same document where Hanzo Edge is a
product. It now states the property. The doc also says plainly what gateway is:
PLUMBING — the trust boundary compiled into cloud as gateway.Mount, not a network
hop — whose one product door is the config plane at /v1/gateway/config. That is
what earns the prefix; the plumbing earns none.
plugin/admin/openapi.json is PRE-EXISTING drift, not part of this change:
apps/admin/o11y.go already said event.log / event.span while its committed subset
still said distributed_logs_v2 / o11y_index_v3, so app-contract was red on main
before this. Regenerated from source, which is what the gate demands.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
7e3bb4af9b |
describe: regenerate the woven document so the prose fix reaches the wire
`make describe` — zipdoc lifts the doc comments, each app projects its own router, the weave composes them. Path count is unchanged at 1058: this moves prose, not surface. It also picks up drift nobody had regenerated: apps/git's zipdoc_gen.go was missing the import/inbound/mirror ops' prose entirely, and apps/admin's had gone stale. Both were invisible because the weave only proves the subsets compose with each other — the drift gate is what regenerates from source. Co-authored-by: Hanzo Dev <dev@hanzo.ai> |
||
|
|
f7e1a72451 |
api: /v1/admin/caps and /v1/admin/volumes
GET|POST /v1/admin/spend-caps, PATCH|DELETE .../spend-caps/:id
-> /v1/admin/caps[/:id]
GET /v1/admin/block-storage -> GET /v1/admin/volumes
Both compounds were qualifiers the address already supplied.
`spend-` : under /v1/admin there is one kind of cap — the per-org usage
ceiling — and the sibling it twins is /v1/admin/promos, not
/v1/admin/plan-promos. The DOMAIN keeps the full term where it is load-bearing:
metering.ErrSpendCapExceeded, commerce's AuthorizeSpendCap and the spend alerts
this forwards to are untouched, because "spend cap" is what the concept is
called wherever it is not already inside /v1/admin.
`block-` : the old comment argued the compound was needed to avoid colliding
with the operator's S3 view at /v1/admin/storage. That is solved better by
naming what the endpoint RETURNS — every row of the answer is a volume — than
by qualifying "storage" twice. /v1/admin/volumes and /v1/admin/storage are now
told apart by their nouns rather than by an adjective.
The operation ids are hand-set on this router (zip.WithOperationID), so they do
not follow a path automatically: adminSpendCaps/adminCreateSpendCap/
adminUpdateSpendCap/adminDeleteSpendCap -> adminCaps/adminCreateCap/
adminUpdateCap/adminDeleteCap, and adminBlockStorage -> adminVolumes. Handlers,
the blockStorageOut envelope and block_storage.go follow to listCaps/createCap/
updateCap/deleteCap, volumesOut and volumes.go.
DigitalOcean's "block storage" survives in PROSE wherever it names the vendor's
product — it is the accurate English for what these volumes are — but no longer
anywhere that reads as an address.
Regenerated: apps/admin/zipdoc_gen.go, plugin/admin/{openapi,mcp}.json, and
openapi.yaml woven from the subsets.
|
||
|
|
a19b879d8c |
api: POST /v1/admin/credits — the method is already the grant
POST /v1/admin/credit-grants -> POST /v1/admin/credits.
A grant is what a POST to a credit collection IS. Spelling it in the address
made the verb appear twice and turned a plain resource into a compound, and it
is the only compound under /v1/admin/{orgs,users,roles,products,compute,money,
bases,flags,…} — every one of which is the bare plural of the thing it manages.
The upstream contract does NOT move: commerce owns the ledger and still serves
POST /v1/billing/credits, the body is still commerce's CreateCreditGrant
forwarded whole, and the type that holds it is still creditGrantIn — that name
is a statement about whose contract it is, which is exactly why it should not
be renamed to match our address.
The operationId is hand-set here (zip.WithOperationID), so it does not follow
the path automatically and had to move deliberately: adminCreateCreditGrant ->
adminCreateCredit, matching the adminCreate<Resource> shape the rest of this
router uses. createCreditGrant -> createCredit and creditgrant.go -> credits.go
so the route, the handler and the file agree.
Also repaired here: the committed subset's prose still said this route forwards
to "POST /v1/billing/credit-grants". commerce renamed that to /v1/billing/credits
and the projection was never regenerated, so the published document has been
naming an address commerce does not serve. Regenerating from the source fixes
it in the same pass.
Regenerated: apps/admin/zipdoc_gen.go, plugin/admin/{openapi,mcp}.json, and
openapi.yaml woven from the subsets.
|
||
|
|
668c63ac33 |
docs: lead every money/identity app doc with the product, not the plumbing
The first sentence of an app's package doc is not internal prose. It projects
verbatim into three places a paying customer reads — the CLI group help line,
the OpenAPI tag description, and the MCP tool prose — so a sentence that opens
"mounts the ... surface", "is the ... plane", or names a /v1 path describes the
implementation to someone who asked what they bought.
Rewrites the opener of 32 app packages across billing/money and
identity/security to state what the customer gets, and reflows the displaced
detail into sentence two. Nothing is deleted: every path, mount note, store
shape and tenancy invariant that was in sentence one is still in the doc, one
sentence lower, where an engineer reading the package still finds it.
billing money door -> your org's balance, what it has spent, the cards it pays with
books "at /v1/books" -> chart of accounts, ledger, bank reconciliation, the reports
o11y "ONE owner of the observability plane" -> your logs, metrics and traces
usage "the usage plane at /v1/usage" -> what your org ran and what it cost
principal "ONE place the data plane turns a request into an org" -> the guarantee
that one org never reads another's data
iam is left alone: "Hanzo's identity provider: users, organizations,
applications, and the OIDC/OAuth2 endpoints every Hanzo service authenticates
against" already leads with the product.
Six of the 32 (finance, payout, metering, money, idv, principal) back no
plugin and so project nowhere; they are rewritten anyway, because the reason
the rule exists does not depend on which reader arrives.
Regenerated openapi.yaml + the 23 plugin subsets from source. The diff is the
description line and nothing else — no route moved.
apps/metering also drops a stale claim to live in the commerce repo; it lives
here.
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 ( |
||
|
|
e247e255cf |
mcp: ONE door — three hand-rolled registries collapse into the typed-op projection
Typing a route bought OpenAPI prose, an SDK method and a CLI command, and NOTHING
on the public MCP surface. zip has projected every typed op into an MCP tool since
v1.18.6 and cloud never called it: manifest routed /v1/mcp to apps/tools, which
hand-rolled its own tools/list + tools/call over a route-table scrape, and
apps/automations hand-rolled a THIRD catalogue. Three registries for one concept,
and the one the public reached exposed none of the 549 typed ops.
THE DOOR IS THE HOST'S. cmd/cloud sets zip.MCPConfig{Path:"/v1/mcp"} and hands
each plugin its own catalogue at Load. The host is the only process that CAN own
it: MCPTools() is in-process, so a plugin cannot enumerate a lazy sibling, and a
plugin-hosted door costs its own wake on the first list. Measured: POST /v1/mcp
beats ai's "/v1" remainder by specificity, not registration order.
THE LIST IS A BUILD ARTIFACT, so tools/list costs ZERO wakes. It has to be: 112
plugins mount LAZILY, and an MCP client calls tools/list constantly — a door that
fanned out over ZAP to ask would destroy the one invariant that makes 112 services
affordable. The answer is already fixed at build time, by the same typed-op
registry that emits openapi.json, so `<app> describe <dir>` now writes BOTH
projections from ONE mount at ONE instant: openapi.json and mcp.json. They cannot
be generated apart, so a tool cannot exist without its op or carry a stale schema.
The leaf plugin/embed.go go:embeds them (cmd/cloud goes 344 → 345 packages, still
zero from apps/). Measured live with the WHOLE fleet mounted: 549 tools listed,
child count 4 → 4 (the four eager apps, untouched).
tools/call is the ONLY trigger and starts exactly one child — p.target(), the same
single-flighted lazy path a prefix request takes — then forwards the SAME message
to that plugin's own /mcp over ZAP on its 0700 unix socket. Never HTTP. The child's
registry answers, so the host can only NAME a tool, never invoke one the child did
not declare. Measured live: get_v1_pricing woke 1 child and returned the pricing
catalog; get_v1_company answered its own handler's "X-Org-Id required" through the
plugin's full cloud.Serve identity chain.
DELETED, not left dark:
apps/tools/builtin.go (223 lines) — the "full-cloud-control" route→tool scrape.
Structurally dead since the monolith died: in the tools CHILD, GetRoutes() sees
only tools' own ~13 routes, and its schemas were opaque {query,body} objects a
model cannot fill. The new door is what it meant to be, with real schemas.
apps/tools/http.go's mcp/mcpToolList/mcpToolCall/rpcResult/rpcError + the route.
apps/automations/mcp.go's mcp/mcpTools/mcpResultObj/mcpErrorObj + its route.
GET /v1/mcp — a Source view that is GET /v1/tools?source=mcp by its own comment.
Principal.credential + credentialHeaders — replay state only builtin.go read.
KEPT, because it is a different capability: apps/tools' EXTERNAL MCP server
registry (records, KMS-sealed secrets, SSRF-validated dialer, tools/list fan-out),
now owning /v1/mcp/servers alone. Its tools, org skills, agents, functions and
connector actions are ROWS, not code, so no build-time catalogue can hold them —
they are reached through the typed POST /v1/tools/call, which is itself a tool on
the door. Nothing lost: connectorToolProvider already published every connector
action into that one registry.
THE GATE. mk/fleet.mk surface-check (which .hanzo/workflows/cicd.yml → hanzo.yml
app-contract actually invokes) regenerates every app FROM SOURCE and fails on
`git status --porcelain -- openapi.yaml plugin/` — mcp.json is under plugin/, so it
was covered the moment it landed there. PROVEN TO FIRE: adding one typed op to
apps/guide without regenerating turned it red on BOTH plugin/guide/mcp.json and
plugin/guide/openapi.json; reverted, green. Four more, all cheap: no App row may
claim /v1/mcp (fiber MERGES byte-identical patterns, so a Load there would shadow
the door silently); no served path may END in /mcp; no Go source outside cmd/cloud
may name an /mcp path unless it is a named foreign engine (apps/tasks' own
surface, which is not a projection of our ops); every catalogue tool must be an
operationId of its own app, unique fleet-wide, with a NON-EMPTY description —
the last one because a nameless tool is a silent failure a model pays context for.
549 tools across 36 apps, 349KB on the wire. zip v1.18.11 → v1.18.12.
Capability check, precisely: the 17 executable connector actions the deleted
automations door listed are NOT tool names on the fleet door, because they are
per-tenant rows — connectorToolProvider publishes every one of them into the ONE
registry from the same `registry` map that door read, so they are reached through
tools_call with the same activation, price, meter and audit. Nothing is lost; one
hop is added. Same for org skills, agents, functions and external MCP servers.
One door this gate structurally cannot claim: /v1/tasks/mcp is hanzoai/tasks' own
engine surface behind cloud's identity gate, mounted on a raw net/http mux so it
is in no subset at all. It is a foreign engine's tools, not a projection of ours,
so it is NAMED in foreignDoors with the reason rather than deleted.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
a660fdb4e2 |
envelope: the list count is total, not data2 — Casdoor's last field name
{status, msg, data, data2} is Casdoor's response type. Its second slot was
untyped, so the row count went there, and the name came along whole — through the
Casdoor Go SDK (hanzoai/iam auth.go still declares Status/Msg/Data/Data2) and
through Casdoor's console, whose getList<T> reads data?.data2 ?? rows.length.
cloud's own envelope.go then called it "the canonical /v1 envelope", which it was
only in the sense that everything had inherited the same shape.
Nothing specifies it. HIP-0111 names it directly as the shape a list MUST NOT
return ("Lists return the SCIM ListResponse envelope (totalResults/Resources),
not a {status,data,data2} one"). Casdoor is dead by standing rule. So the field is
`total`, which is what it holds — same int, same position, a name a reader can act
on. 142 sites, 30 files, plus the doc-comment examples zipdoc lifts into the
published spec.
ONE EXCEPTION, and it is the point of the change rather than an escape from it:
apps/admin/iam/iam.go DECODES hanzoai/iam's answer, and IAM still writes data2.
That struct now reads `Total json.RawMessage \`json:"data2"\`` — the Go side speaks
cloud's language, the tag records the foreign wire, and a comment says when it
converges. A blind rename there would have read nothing: the total silently
becomes zero and every paginated admin list reports its own page size. The same
trap was live in the tests, whose fake IAM server writes data2 on purpose — four
tests caught it, which is why they exist.
The console (hanzoai/admin b030f80) ships the reader in the same window and
accepts total → data2 → rows.length, so either side can deploy first.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
f539538692 |
pricing: PATCH providers/{name} becomes an op, and four apps stop mis-describing embedded fields
TWO THINGS, ONE CAUSE — zip v1.18.11.
The route. PATCH /v1/admin/catalog/providers/{name} was refused because `overrides`
is an RFC 7386 merge patch stored and echoed verbatim, which pins its Go type to
json.RawMessage, which zip published as an ARRAY OF INTEGERS. typed_wire_test.go
had pinned that lie deliberately, with instructions: "If zip now describes a raw
JSON value as one, the providers/{name} reason is stale — type the route." v1.18.9
describes it as one. The test went red, and this is the route it asked for.
The overlay upsert is now applyPatch(ctx, kind, id, patch) with no request in
sight, called by the typed op AND by the *zip.Ctx door the models/* wildcard route
still needs — one implementation, two doors, so they cannot drift. The patch
fields stay POINTERS: absent must differ from a zero the caller meant, and
encoding/json leaves a pointer nil for an explicit null too, so {"enabled":null}
and {} arrive identically — which is what this route already did.
One residual delta, recorded in ops.go rather than hidden: zip decodes before the
handler runs, so a non-admin sending malformed JSON now sees 400 where the raw
handler answered 403. zip's authorizer is deliberately post-decode (it authorizes
the decoded value, so the decision cannot diverge from execution), so this is not
avoidable while the route is an op. It reveals only that the body was unparseable.
The four apps. Declaring that op exposed a defect in v1.18.9 itself: its published
requestBody was ABSENT. The input is a path param plus an embedded unexported
patch body, and every projection asked "what fields does this type carry" with its
own loop over NumField — so it skipped the embedded type on IsExported and saw
only `name`, which IS a path param. wireFields (v1.18.11) is the one function that
knows encoding/json's promotion rule. Regenerating with it corrected four apps:
pricing gains the request body it always accepted;
admin publishes SaaSMetrics' fields flattened, where it had a property
literally named "SaaSMetrics" that the wire never sends;
agents gains 117 lines of response fields that were published nowhere;
visor gains 81 lines, same cause.
Every one of those was a document describing a shape the service does not have,
in openapi.yaml and in every SDK generated from it. Found by READING the artifact
after the bump instead of trusting that the bump was an improvement.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
de9b59624f |
zip v1.18.9: the document stops saying four untrue things, and authz stops publishing a removed API
The bump alone changes 23 published artifacts, because zip v1.18.9 fixes what the projection SAYS rather than what any route does. No wire moves. phantom request bodies 41 -> 9. A POST binding its whole input from the path published a required body whose only property was the path param, so every generated SDK gained an argument the caller must build to repeat a value it already passes in the URL. The 9 left are the raw-body family (git-upload-pack, bank-statement import, a deck upload) — they eat bytes, not JSON, and owe a binary content type via openapi.Binary rather than an empty object. time.Time stopped publishing as a $ref to a schema with no properties and now says format: date-time. Its fields are unexported, so reflection over them described nothing: every timestamp in every generated SDK was untyped. summaries lost their embedded line breaks — one sentence on one line, which is what the spec, the CLI's one-line help and an SDK's first docstring line all want. imported types' FIELD docs reach the document at all. zipdoc matched the parsed and type-checked views of a struct by byte offset, which only agrees for a package loaded from source; an imported type's position comes from export data with a synthetic offset. So every op whose In and Out live in the call plane published a description and zero field descriptions. AND ONE STALE PUBLISHED SURFACE, which the regeneration exposed rather than caused. plugin/authz/openapi.json documented GET, POST and DELETE /v1/authz/policies. hanzoai/authz v1.10.15 does not serve them, and says why in serve/mount.go: the grant set belongs to IAM, "a second writable copy behind this surface would be a second source of truth for who may do what". So cloud was advertising a writable authorization-policy API that had been deliberately removed, and three methods in every generated client answered 404. Verified as pre-existing by regenerating the subset at origin/main on the OLD zip: the same three paths vanish. That drift means the gate was red on main and stayed red. It is invoked (hanzo.yml:134), which leaves the two ways it could have been red and unnoticed — worth a look, not a guess. Also: openapi-apps now honours OPENAPI_NEEDS_BROKER, which only openapi-check did. The gate's own failure text says "fix: make openapi", and that fix routed through openapi-apps, which mounts kafka, which fails closed with no broker — so the single command told to repair a red gate could not run. One exemption list, read everywhere it applies. Co-authored-by: Hanzo Dev <dev@hanzo.ai> |
||
|
|
49f8eeecd9 |
zip: take v1.18.1, and let the document say what the handlers already knew
zip v1.18.x closes four gaps between what a typed op DOES and what its document
SAYS. Taking it regenerates the published spec — the file the SDK repos pull —
and unblocks the migration these routes were waiting on.
* A typed DELETE no longer reads a request body; its input is the URL. All 20
of cloud's DELETE ops keep working: 15 take only path params, and the other
5 take scalars that bind from the query exactly as the document already said
they would. DELETE /v1/marketing/suppressions is the one with no path param
at all, so its whole input is now ?channel=&address= — which is what a
client generated from openapi.yaml has always sent.
* A URL-borne field's `validate:"required"` reaches its parameter, so an
argument the handler refuses to run without stops being described as
optional.
* A bodyless op's example survives. openapi.Parameter had no name for it, so
the round-trip through Typed() dropped it — exactly the "dropped honestly,
if nothing here has a name for it" its own doc comment warned about. Every
GET and DELETE reached the published reference with no example at all;
adminDeleteSpendCap now carries example: cap_1 on the path and acme on the
query, from the one Example its doc comment already had.
* A path parameter is typed from the field it binds to, so ?sizeGiB= is an
integer in the document because it is an int in Go.
openapi.yaml is 245 lines SHORTER despite 128 new example/required/query lines:
a named struct is now one definition every op $refs instead of being inlined at
each use. 148 schemas across the admin subset alone.
probe/ is inverted, which is what it told whoever came next to do. It pinned two
zip limitations — a typed op cannot see its own URL, and a templated path is
emitted with no parameter object — and both are gone, so it now asserts the
capabilities: the whole URL binds, and /v1/agents/sessions/{id} declares its
parameter, typed. The 16 of 25 clients/agents routes that carry a path param
are unblocked. What stays pinned is the DEFAULT, not a framework limit: an op
with no Authorize installed answers an anonymous MCP caller.
commerce moves to v1.49.30 for the same reason — Mint decorates a zip.Router,
so it now answers for where a typed op lands, gate included.
Pre-existing failures unchanged (functions/provisioning/storage/kmsreseal need
billing config, S3 and a real KMS); the two probe failures are fixed.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
85dd3a6513 |
name: a thing the host loads is a Plugin
MountSpec was a compound naming a struct after the mechanism that consumes it. The thing it describes is one of the plugins the host loads: name, price, mount. The directory is plugin/, the framework is the zip plugin framework, and every doc comment already called them plugins in prose. So: Plugin. Not App, which was the obvious first choice and is wrong twice over -- package cloud already declares an App in payloads.go, and the struct itself carries an App field for a subsystem that gates the whole binary. Either collision alone would have made the name ambiguous at every use site. Mechanical: 132 files, plus the parameter and loop variables that carried the old noun (specs, spec, sp) to the noun they actually hold. |
||
|
|
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> |