main
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
de97f6dfb7 |
the projections say what they do, and the one that said nothing is counted
A typed op mints five things from one registration. Three defects here let an
operation reach all five carrying no sentence, and one hand-written file
described a surface nobody generated.
POST /v1/exec published a summary and no description. The handler had a doc
comment; the package had no zipdoc directive, so nothing lifted it — and the op
registered on the cloud.Router parameter, which zipdoc cannot follow to a prefix,
so it refuses to lift even with the directive. openapi.Complete accepts EITHER a
summary or a description, so every gate stayed green over the hole. The
registration moves to the *zip.App (the apps/meet and apps/blueprint pattern);
routes move, the credential middleware stays on the scoped router where the
prefix guard applies to it. Its 15 published fields carry prose now too.
docs/automations-openapi.yaml is deleted. 25 KB of hand-authored OpenAPI over 17
operations of /v1/automations, referenced by nothing and compared by no gate — so
it had drifted the way a second copy always does: it claimed two operations the
fleet does not serve (GET /v1/automations/health, POST /v1/automations/mcp) and
omitted three it does (connectors/{id}/run, flows/{id}/versions,
hooks/{source}/{event}). The document already describes that surface.
The field tranche closes 343 published properties across seven apps — authors
39->0, label 37->0, channels 34->0, prompts 29->0, leaderboard 40->0, campaign
42->6, affiliates 123->10 — and campaign's two result types stop being defined
types over another struct, which published all 30 of their properties bare; the
struct is declared under its published name and the domain name is an alias, so
there is still one shape.
The 16 that remain are not app defects and are recorded as such in LLM.md: zip
keys a promoted field's prose under the type that DECLARES it and looks it up
under the type that PROMOTES it, so no comment written here can reach them.
LLM.md records what the running deployment measures, taken by making the request:
the served document is the committed one at the revision the header names (1735
paths / 2474 ops, identical sets), /v1/commands answers 2448 commands under a
working ETag, POST /v1/mcp lists 88 tools and tools/call returns lifted prose —
and those tools reach only 1189 of 2422 operations, with 134 declared refusals
accounting for a tenth of the gap.
|
||
|
|
d09ff7317b |
exec: the tenant is not a header, and the credential is not a path
Red returned do-not-ship on two CRITICALs that chain to unauthenticated
cross-tenant code execution. Both are proven by apps/exec/auth_test.go, and all
four assertions FAIL when the old behaviour is put back — mutation-checked, not
asserted.
1. THE TENANT CAME OFF A HEADER. callCtx preferred cloud.Who(ctx).Org, which is
zip.CallerOf, which reads the X-Org-Id REQUEST HEADER (zip caller.go:377) —
and for a request with no validated bearer, SanitizeIdentity deliberately
RESTORES the client's own header (middleware_identity.go:455). So
`X-Org-Id: victim-corp` made storeFor open the victim's SQLite file: the run
executed in their store and /v1/files + /v1/download read their artifacts
back out. principal.OrgFrom is the org a VALIDATED principal resolved to and
nothing else; every other app resolves through it and this one did not.
plane.go's own note — "an org in the argument is an org the caller chose" —
is the rule it was breaking.
2. THE CREDENTIAL CHECK WAS A LOWERCASE PREFIX LIST OVER c.Path(). fiber routes
case-insensitively; cloud.RoutePath exists in this repo for exactly that and
two other gates already use it. POST /V1/EXEC matched the route and missed
the list: no key at all ran code, a wrong key read another session's bytes,
and CODE_EXEC_API_KEY UNSET — the documented fail-closed 503 — still ran code.
Same root, so one fix: authorization stops being inferred from the SPELLING of a
request. The middleware normalizes with RoutePath AND parks two facts on the
context — principal.WithOrg (inherited by the typed op, typed.go:82) and an
unexported `admitted` marker. tenantOf is the ONE tenant decision and refuses a
context carrying neither. The prefix list may still drift; drift is now
fail-CLOSED — a missed path is a 403 on a route that should have worked, never a
route that works without a credential.
AND THE DOOR NO LIST COULD HAVE COVERED. A typed op is also an MCP tool and an
op-plane op, and tools/call invokes it DIRECTLY (zip typed.go:474) — no route, no
middleware. `POST /mcp name=post_v1_exec` with no key ran code. Typing /v1/exec
for its SDK value is what opened that door; the handler-side check closes it,
because those doors park no marker and carry no principal.
3. [HIGH] AN EXEC SANDBOX HAD NO CEILING. Single-attach bounds dev/desktop via
their project; an exec sandbox has none, and the code tool sends no
session_id, so every call mints a fresh pod on a 15-minute lease — 40 calls,
40 pods, each 250m/512Mi/2Gi. The reaper is the FLOOR, not the ceiling: it
ends leases that are over, bounding the steady state and never the burst.
maxLiveExec=16 is written in node capacity (8Gi, 4 cores) and refuses with
429, because the caller's correct response is to wait. Counted with a real
COUNT(*), not len(List) — List is LIMIT 200, which stops counting exactly
where refusing starts to matter.
4. [HIGH] TWO CLIENT-SHAPE BUGS, both silent. hanzo.chat primes an attachment as
{id, session_id, name} (Files/Code/process.js) while @hanzochat/agents spells
it storage_session_id (tools.d.ts) — reading only the second skipped every
attached file AND the "not available" note, so a user's CSV was invisible with
no error. CodeFile.Session() reads both, and a ref with neither is reported.
Artifacts were COLLECTED recursively (find) and LISTED top-level (ls -1A), so
a nested artifact appeared in the reply and was missing from /v1/files/{sid},
which the client reads as expired. One find answers both now.
Also: apps/functions.go's doc no longer cites the deleted CODE_EXEC_UPSTREAM.
Tests: 22 in apps/exec (14 + 5 auth + 3 client-shape), 6 in apps/sandbox. Full
suite 3 red — base, code, commerce — all pre-existing and environmental (this
box's SQLite lacks fts5 and acos). Zero new failures. make check green.
NOT FIXED, and it is a decision rather than a defect: the real chat client sends
no credential at all (EnvVar.CODE_API_KEY undefined, handleTools.js sends no auth
header, the chat pod has no LIBRECHAT_CODE_API_KEY), so the guard 401s every
legitimate request. Fixing the bypasses does not make the feature work; settling
what the client presents does, and the house rule says Hanzo IAM rather than a
second shared secret.
|
||
|
|
48c2af53f9 |
a session is a sandbox: exec stops proxying to a Service with no endpoints
apps/exec was a reverse proxy to code-exec.hanzo.svc.cluster.local:8000, and
that Service has had ZERO endpoints for 33 days — /v1/exec answered 503 in
production the whole time. It was not failing to reach the executor; there was
no executor. apps/functions read the same CODE_EXEC_UPSTREAM and was failing
against the same absence.
cloud already runs the one compute primitive. A LibreChat session IS a sandbox
("a code-exec call = a sandbox with a session lease", apps/sandbox's package
doc), so session_id is the sandbox id, upload/download/list are Write/Read/Read
on that sandbox, and exec holds no store, no session table and no lifetime of
its own. The lease ends on the reaper.
THE BLOCKER, AND THE DECOMPLECTION. Every sandbox operation existed ONLY as
`func X(s *Service, c *zip.Ctx) error` — the domain braided into the transport,
so nothing else in the process could use a sandbox. apps/sandbox/api.go is the
domain as a VALUE: Lease, Get, List, Run, Read, Write, End. The HTTP handlers
become adapters (bind, call, JSON) and apps/sandbox/plane.go is a SECOND adapter
over the same functions. Errors are zip errors in the core, so "not yours" is
404 once, decided where the fact is known.
(Functions taking *Service, not methods: `Service = cloud.Service[state]` is an
alias for a generic type in package cloud, and Go cannot define a method on a
non-local type. apps/git's files.go is the same shape for the same reason.)
cloud.Ask AND NOT A GO IMPORT, and that is correctness, not style. Apps ship as
separate plugin binaries (plugin/<app>/main.go "links only its own subsystem"),
so importing apps/sandbox would give exec a SECOND sandbox service — its own
OrgStore on the same per-org SQLite files, its own reaper racing the real one.
plane.Ask already collapses to an in-process dispatch where the fleet fuses two
apps (zip.Serving/zip.Here), so the ONE call is a function call when they are
co-resident and a socket hop when they are not.
THE CONTRACT IS MEASURED, from ~/work/hanzo/chat, not remembered. Download is
TWO segments — /download/{session_id}/{fileId} (crud.js) — not /download/{id}.
Upload answers {message:"success", session_id, files:[{fileId, filename}]} and
the client throws unless `message` is that literal. /files/{sid} answers a BARE
JSON ARRAY of {name, lastModified} whose name is that same two-segment
identifier. And the code tool tells the model to persist artifacts in /mnt/data,
so sandbox gains workdirFor(class): /mnt/data for exec, /work for dev. Listing
/work would have reported no files after every successful run.
/v1/exec/programmatic answers 501 rather than being routed into the interpreter.
It is a DIFFERENT protocol — a run suspended on each tool call and resumed from
a continuation token — and answering it with a Result its parser cannot read is
a wrong answer where a refusal is an actionable one.
TENANCY, and the trap. cloud.For states a caller only on a context with NO
request behind it: zip.CallerOf reads the request's headers first, deliberately,
so no handler can assert an org a caller did not arrive with. A typed op's ctx
HAS a request behind it, so cloud.For(typedCtx, org) silently states nothing and
the peer answers 403. exec.callCtx is the one rule — pass a ctx that already
carries a principal through unchanged (hanzo.chat forwards the user's IAM
bearer), otherwise detach to Background, state the deployment's brand org, and
put the request's cancellation back with context.AfterFunc.
apps/functions moves to the same door, because task 3 (deleting the orphan
Service) would otherwise turn its 502 into a DNS failure. It calls exec.Run —
one operation, one home — and ENDS the lease, because a function invoke is over
when it answers.
THE FLOOR IS LOWERED HERE, next to its reason. exec published 40 operations
because a proxy cannot describe a contract it does not own; it now implements
one, so it publishes 5 — POST /v1/exec is a TYPED op (CodeRun -> CodeResult),
which is the schema, the MCP tool, the CLI command and the SDK method a proxy
could never carry. The four that stay untyped have real reasons: multipart in,
bytes out, a bare array, and a protocol we do not serve. Numbers taken from the
ratchet's own report, not computed:
paths 1762 -> 1759 (-3)
operations 2465 -> 2430 (-35)
download 10 -> 1 · exec 10 -> 2 · files 10 -> 1 · upload 10 -> 1
Deleted: newProxy, defaultUpstream, CODE_EXEC_UPSTREAM, and the ledger of 40
untyped operations. CODE_EXEC_API_KEY stays — it is the credential the chat
presents, and the guard is now middleware because a typed op takes no handler
chain.
Tests: apps/exec 14 (a sandbox peer double on the REAL plane, so an op renamed
or a field moved fails here and not in production), apps/functions green
including TestInvoke_AllowsAndDebitsCallerOrg, which was RED before this change
— metering moved debits onto the plane and the test still had only an HTTP
double.
Measured against HEAD: 12 packages fail before and after; zero new; one fixed.
LLM.md also carries a concurrent agent's insights.hanzo.ai section, present in
the tree and committed rather than dropped.
|
||
|
|
ceff43ac30 |
the document publishes what was declared, not what the router happens to bind
Two ways the same confusion showed up, and one build that could not run. TRACE and OPTIONS were published — 33 each — because All() binds every method at a path and the describe loops read that route table as a product surface. CONNECT and HEAD were already excluded on exactly that principle. TRACE echoes the request back; it is the Cross-Site Tracing verb and should not be reachable on a public API at all, so advertising it in the contract is worse than merely routing it, because the contract is what the SDKs, the CLI and the MCP tool list are built from. OPTIONS is CORS preflight: a browser sends it, a person never does, and `hanzo meet options` is not a command anyone wants. Between them, 66 operations — and the generated CLI already carried none of them, so this is the document agreeing with the projection that was right first. websearch held its own copy of that method list. It said seven, including the two, and the day the document stopped publishing them the copy went on describing operations that no longer existed — which openapi.Methods() exists to prevent and which the describe gate caught by name. It reads the projection's own set now. The floor drops by exactly 66, in this commit, next to the reason. Separately: every app Makefile claims "Generated by plugin/gen-app-cmds" and none of them were. The generator wrote plugin/<app>/main.go and left a human to remember the second file, so `sandbox` arrived with a main and no Makefile, so `make describe` had no rule for it, so it published no subset, so `make openapi` failed on an app that was otherwise finished. The generator writes both now, from the same manifest row, taking the package from each app's own composition root rather than a lookup table. It reproduced all 117 existing Makefiles with zero APPS values changed, which is the evidence the rule is the one already in use. External modules fall out structurally — no package directory here, no Makefile — so mk/fleet.mk's EXTERNAL list is not copied. |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
1f3fbe84ed |
exec: 0 typed of 56, and the refusal stops being prose
The code-interpreter surface publishes 56 operations and not one carries a
description, a schema, an MCP tool, a CLI command or an SDK method. It is not a
backlog item: Mount hands all 8 paths to httputil.NewSingleHostReverseProxy and
the sandboxed executor supplies every byte, every Content-Type and every status,
so there is nothing here to describe and four wire facts a typed op would move —
verbatim upstream status (zip answers its own declared one, typed.go:305-311),
response fields this repo never named (an Out drops them), a multipart
/v1/upload body (typed.go:242 jsonenc.Unmarshals every non-empty body) and a
byte-bodied /v1/download/{id} (typed.go:311 always c.JSONs). 16 of the 56 are
OPTIONS/TRACE, which zip has no typed registrar for at all. openapi.Register is
refused too rather than reached for: it could only publish a guess at
@librechat/agents' contract, and Binary's application/octet-stream is not a
multipart envelope.
So the refusal becomes a gate. typed_wire_test.go crosses two closed lists
(untypedPaths x servedMethods) into the same 56 addresses the document uses and
fails three ways: a route neither typed nor named, a reason naming an address
this mount no longer serves, and the day one of them becomes typable. Eight
sub-tests measure the wire facts through the REAL Mount rather than asserting
them. Both directions were mutation-checked: a fifth prefix in exec.go and a
stale ledger entry each turn it red.
Two findings the pass surfaced, recorded in LLM.md rather than fixed here:
- The migration's own inventory cannot see this app. Every documented grep
anchors on `("` or `("/`, so `app.All(p, h)` in a range over prefixes matches
nothing — apps/exec reads ZERO under both commands while serving 56
operations, which is why it has never appeared in a tranche. Same shape in
apps/knowledge (subsystem.go:61-69), apps/iam (258-259, 282) and
apps/commerce (mount.go:551). Count operations, not lines.
- plugin/exec/main.go told the next reader to "edit the spec below directly".
There is no spec in main.go, and the one it means is generated by
`make -C apps/exec openapi` and must never be hand-edited. Corrected here;
110 sibling mains still carry the sentence.
exec.go gains only a comment; the regenerated subset is byte-identical, which is
the honest measurement: 56 operations, 0 described, before and after.
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> |