main
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4938b9149e |
close the four gates inc2's commits opened
Merging inc2 onto forge surfaced four failures. All four were the gates working;
none was merge damage.
principal.ProjectFrom / WithProject — apps/crawl's scopeOf reached for
cloud.Request, the pinned escape hatch, to read two facts: org and project.
OrgFrom already existed; Project had no ctx counterpart, so the hatch was the
only way across the typed-op seam. It has one now, and crawl reads the context
like everything else.
WithProject parks UNCONDITIONALLY, which is the one asymmetry with WithOrg and
is deliberate: an org is an AUTHORITY, so an unvalidated request must park
nothing rather than an empty tenant a query would honour. A project is a
NARROWING — every consumer ANDs it with an org that already gates — so gating it
here would state the authority twice and let the two statements disagree.
agents/builtin_test — the test asserted the default assistant carries no tools;
the code now offers it the whole door. The CODE is right: an empty offer reads
to a model as "there is nothing here", and the assistant was reporting it could
not reach the cloud while the door served 88 tools one socket away. The test
sentence was the stale half, so it now says what is true.
floor tracker 10 -> 9 — GET and DELETE on /v1/tracker/projects/{key}/issues/{num}
stopped registering, and that is deliberate: tracker moved to reading the forge
(
|
||
|
|
7d42283fdd |
tracker: the board reads the forge, and the forge says who is asking
CI/CD / containment (push) Successful in 3m39s
Hanzo CI/CD / cicd (push) Failing after 1h1m51s
CI/CD / gate (push) Failing after 1h1m51s
CI/CD / image (push) Skipped
CI/CD / rollout (push) Skipped
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Skipped
The work items on /v1/tracker were a SQLite table beside a github.com feeder, while the estate files, labels and closes its issues on git.hanzo.ai. Two stores under one prefix are two answers to what the state of a piece of work is, and they disagreed the first time anyone touched the forge directly, which is every day. The forge is now the store: every read is a read OF it, every write a write TO it, and nothing here caches or mirrors a row. A board is a repository, a column is a LABEL, and a card is an issue. Reading the column off a label is what makes the board and the forge web UI the same object seen twice — relabel in either and the card moves in both. So the repository lifecycle is NOT on this surface: creating, renaming and deleting a board are forge operations under forge permissions, and a second door onto them here would be a weaker guard on the same object. Those three answer 405 naming the forge, which is a different fact from 404. Milestones are repo-scoped upstream and there is no org-level list, so the org view is a server-side fan-out over the repositories the caller can see — bounded, and failing whole rather than returning a partial rollup that reads as complete. TWO INDEPENDENT CONTROLS, because neither is trusted to be sufficient and this forge really does host private orgs. The org comes from the validated principal (principal.OrgFrom) and never from a path, query or body. Then every call is made with Forgejo Sudo as the caller's own IAM username, which DROPS PRIVILEGE to that user — measured against the live forge: the machine token reads hanzo-private/patents (200), the same token sudoed as a non-member gets 404, byte-identical to anonymous. So a bug in the first control cannot leak a private repository on its own, and a write is attributed to the HUMAN rather than to a shared bot. One credential, held in KMS at orgs/hanzo/deploy/FORGE_TRACKER_TOKEN@prod — never an env file, never a browser-side PAT. A separate secret from the universe pin token on purpose: one credential per capability, so a compromise of the tracker cannot deploy. Resolved lazily with a TTL so rotation is live without a restart, invalidated when the forge rejects it, and fail-closed at every step — an anonymous client would quietly serve public repos and read as "your board is empty" rather than "this deployment is misconfigured". The forge host is brand.Sibling of the deployment's own API host, so a white-labelled deployment cannot read another brand's forge. Also closes two tenancy defects found beside this work: plane.AgentPRIn carried an Org the agent-PR seam read off the wire and passed straight into the per-tenant store selector, so a caller on the plane could file a work item onto ANOTHER tenant's board by naming it. Its sibling on the same socket, IssueIn, has never had one. The field is gone, the handler reads cloud.Who(ctx), the caller carries the org in the ENVELOPE (which is re-checked by the same OrgOf rule as the HTTP boundary), and a reflection test now fails if an org-shaped field returns to either input. The audit trail's Home field means "a platform SuperAdmin acted inside another tenant". Its predicate was home != effective, which WAS impersonation back when a SuperAdmin was the only principal who could act outside their home org. Since membership-based org switching, any ordinary member of two orgs trips it the moment they work in their second one — telling auditors that routine work was an admin impersonation, and burying the real events in volume. The predicate is now the fact it always meant: home is the reserved admin org (authz.AdminOrg, the issuer's constant). Tests: 22 on the forge client (fail-closed with no actor, credential never in an error, bounded fan-out, pagination that terminates against an endless forge, a compile-time refusal of tenancy in the filter), 12 on the surface (cross-tenant read, no-principal refusal, CSRF-refused writes never reaching the forge, attribution of a move). The retired SQLite HTTP surface's tests go with it; the two security properties they held — the CSRF gate and the per-IAM-project store isolation, which still backs the plane doors — are re-pinned against what survives. forge/live_test.go exercises the real forge, skipped unless FORGE_LIVE_TOKEN is set, because a stub can only confirm we built what we believed and not that what we believed is true. Co-authored-by: Hanzo Dev <dev@hanzo.ai> |
||
|
|
7c50638eb1 |
merge inc2/main: one line again
CI/CD / containment (push) Successful in 1m55s
Hanzo CI/CD / cicd (push) Failing after 6m48s
CI/CD / gate (push) Failing after 6m49s
CI/CD / image (push) Skipped
CI/CD / rollout (push) Skipped
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Skipped
The two private lines had diverged 191/18 off a merge-base six hours old. Production builds from inc2; every push went to forge; neither knew. The 14 conflicting hunks, and why each resolved the way it did: - bridgeReply loses its ctx parameter (inc2). Both lines fixed the same production bug — the webhook's ctx made zip drop the stated org, so the balance gate answered "no org on the call". forge passed a detached ctx in; inc2 deleted the parameter so the webhook's cannot be passed at all, and states the tenant with cloud.For. Unrepresentable beats discouraged. Its body now calls forge's bridgeIdentity, so the link lookup exists once. - commerce prefixes are the union: forge's /v1/cart and the /v1/billing/topup stem, inc2's accounts, credit-balance and transactions. topup/token is dropped — the stem owns its subtree. A prefix missing here never reaches commerce; it falls to ai's bare /v1 and answers ai's 404. - zipdoc-check keeps inc2's target (forge repeated the loop twice) with forge's body: per-package, because whole-module load extracts differently than the generator it polices, and skipping dot-dirs, because an agent worktree is a second checkout and the walk read 203 packages where there are 104. - plane.go keeps both new sections — sandbox ops and the coding-run seams are orthogonal, no shared names. - plane_debit_harness_test.go takes forge's, which delegates to internal/planetest instead of holding a second copy. Same API. The copy is what hid a bind failure: a unix path is 108 bytes and t.TempDir() spells the test's name into it. - metering keeps Actor and the token counts; plane.Usage has the fields, and a debit without them can be re-read but not re-derived. - o11y v1.5.62 over v1.5.61 — upgrade, never downgrade. go build ./... and go vet clean. Full suite: 3 red (base, code, commerce), identical to both parents — this box's SQLite lacks acos and fts5. |
||
|
|
88d3bee3d7 |
integrations described slack/install twice, so thirteen apps could not build
CI/CD / containment (push) Successful in 2m24s
Hanzo CI/CD / cicd (push) Failing after 51m20s
CI/CD / gate (push) Failing after 51m20s
CI/CD / image (push) Skipped
CI/CD / rollout (push) Skipped
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Skipped
apps/integrations carried two openapi.Describe calls for GET /v1/integrations/slack/install, written by different hands into different init() funcs. Describe panics on a duplicate — correctly, because two descriptions of one operation means one of them renders and nobody can tell which — and that panic fires at init, so it took down every app that links integrations: ads, automations, campaign, catalogsync, channels, cloudflare, company, content, destinations, git, guide, integrations and sync all failed to describe. The earlier one survives. It was already the superset: it has the attribution constraint (Slack refuses a slack.com URL in that field, so the click has to route through an address of ours to be counted) AND the tenant point (public, no principal, binds no org, because minting an org for an anonymous click is the one thing that would break isolation). The later one had a single fact the first did not — 503 where the app is unconfigured, rather than a consent URL with an empty client_id that Slack renders as its own dead end — so that sentence moved across before the duplicate went. The floor drops for the merge's own deletion too: /v1/billing/gpu/charge and /v1/billing/gpu/eligibility are gone because GPU is metered like any other resource now, and the bespoke prepay path with it. Checked rather than assumed — a -1 that is not a multiple of two is not a TRACE/OPTIONS removal, and an unexplained shrink is exactly what the ratchet is there to make someone look at. 1762 paths, 2480 operations, 185 products. Every one carries an operationId and a summary; 51 still want a long description and 49 of those are hanzoai/ai, whose prose belongs on its controllers, in that repo. |
||
|
|
5435fe3a05 |
slack: @hanzo answers out of the box, and the agent can call tools
THREE things, all measured against production tonight.
1. AGENT NOT FOUND. With the plugin-boundary fix deployed the turn finally
reached agents, which answered `agents: agent not found`. The bridges ask
for the conventional ref ("hanzo") and Store.Resolve is a plain row lookup
with NO seeding anywhere — so an org that connected Slack and did nothing
else had no agent, and @hanzo could never work out of the box in ANY
workspace. The conventional ref now resolves to a BUILT-IN default: not
persisted (a row would fork the definition per org and strand already-seeded
orgs on a later change), and a row the org DOES create still wins because
Resolve is tried first. An unknown ref stays a miss — silently substituting
the chat agent would make a typo in `code: repo` run the wrong thing and
look like it worked.
2. TOOL CALLING. executeRun did one completion and returned the text; Agent
.Tools was stored, updated and displayed but never read by a run, so the
agent could not reach anything. It now runs a bounded tool loop and takes
the actor, so every tool call is attributable to the org and user that
caused it.
3. TOOL SURFACE. fleet/mcp.go + fleet/surface.go curate and gate what a client
may see: the live server projected 1,323 internal ops with zero annotations,
and Slack saved the first 128 ALPHABETICALLY — a window containing zero
product tools and 36 credential/auth ops including CreateServiceAccountKey.
Cross-agent collisions found and fixed while integrating: tools.go's truncate
collided with an existing test helper (renamed truncateToolResult, which says
what it bounds), and executeRun gained an actor parameter that two test callers
had not been updated for.
go build ./... clean. fleet and plane suites green; apps/agents' one failure,
TestTargetOpsProjectEverywhere, is the known pre-existing op-id drift.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|
||
|
|
ad15df5781 |
openapi: regenerate the subsets — the ai door renamed three resources and gave permissions back to IAM
Hanzo CI/CD / cicd (push) Successful in 18s
CI/CD / gate (push) Successful in 18s
CI/CD / containment (push) Successful in 1m13s
CI/CD / image (push) Successful in 18s
CI/CD / rollout (push) Failing after 11s
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Failing after 1s
plugin/ai/openapi.json was last written at |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
2ce4bdf70d |
feat: type the authors/base/campaign/legal/tracker surfaces — 39 ops, one registry entry each
Five plugin subsets published 56 operations with ZERO descriptions: no schema, no
prose, no MCP tool, no CLI command, no SDK method. 39 of them are now typed ops,
which is ONE registry entry with N projections — the REST route, the OpenAPI
operation, the /mcp tool, the CLI command and every generated client all follow
from the same declaration.
authors 0/11 -> 11/11 described
legal 0/11 -> 10/11
campaign 0/11 -> 9/11
tracker 0/10 -> 8/10
base 0/13 -> 1/13
fleet golden: 517 -> 556 described, 1407 operations unchanged.
THE WIRE IS UNCHANGED, and that is the point of the exercise. Six details had to
be carried over deliberately rather than inherited, each pinned by a test:
* the 1 MiB request-body cap in apps/legal, as ONE middleware in FRONT of the
typed ops. A typed op receives its DECODED In, so a size check inside one
runs after the parse it exists to precede — zip answers 400 about unparseable
bytes where this package has always answered 413. 403 still outranks 413, and
the signature completion (which discards its decode error) is deliberately
not capped.
* the body REQUIREMENT on every write that bound one with c.Bind. zip's typed
decode is TOLERANT by construction, so a naive conversion turns a bodyless
PATCH from 400 into 200-with-nothing-changed — measured against the untyped
handlers, not assumed.
* 201 where it is unconditional (legal's two creates, campaign's create),
DECLARED with zip.WithStatus so the document keys its response on the code
the route sends. Where it is CONDITIONAL (authors connect/verify/record answer
201-on-create and 200-on-found from one address) cloud.Created sets the code
the route has always sent and the prose states it — zip cannot declare two
success codes for one op.
* Cache-Control: no-store on legal's two document reads, which carry contract
text.
* the JSON ARRAY the tracker listings answer: a NAMED slice Out, because an
unnamed one publishes no response content at all.
* the conditional keys of legal's document view, as two Out types rather than
an omitempty that would also drop an empty body from the single read.
SEVENTEEN operations stay untyped, each for a MEASURED wire fact, each held as a
closed list a new route cannot join by accident:
* 12 x /v1/collections[/*] (base) — a VERBATIM reverse proxy to the managed
Base orchestrator. Status, headers and bytes are the upstream's; no Out can
carry that.
* POST /v1/tracker/projects and .../issues — the pre-create balance gate renders
its denial with cloud.DenyResource, the fleet's NESTED {"error":{...}} at
402/503, which a typed op's returned error cannot carry.
* POST /v1/campaign/{id}/launch and /pause — neither has ever read a request
body, and zip's invoke refuses one it cannot parse BEFORE the handler runs.
Measured: 200 untyped, 400 typed.
* POST /v1/legal/documents/{id}/sign/complete — DISCARDS its decode error, so
an unparseable body still drives the provider-reported completion.
The last two are the same zip gap: an op cannot declare that it takes no body, or
tolerates one it cannot parse. hasRequestBody already computes "binds nothing the
URL does not carry"; invoke reading the body only when that is false closes both.
Nine published schema names collided with already-published ones (Campaign with
marketing, Filing/Signer with company, Template with guide, Metrics with agents,
repoView with git, projectView with platform, healthReport twice). openapi.Weave
refuses one name meaning two things, and it refused this. The colliding types are
renamed on MY side only, as DEFINED types over the same structs — the json tags,
and therefore the wire, are byte-identical.
Also: registers the four new cloud.Request call sites in the escape-hatch pin with
the reason each one needs the request (the project sub-scope that picks a physical
store, the SuperAdmin claim, the audit ACTOR, the body gate).
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> |