main
19
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.
|
||
|
|
b9b516ae17 |
Merge remote-tracking branch 'forge/main' into feat/sandbox-executor
CI/CD / image (push) Failing after 12m49s
CI/CD / gate (push) Successful in 27m39s
CI/CD / containment (push) Successful in 3m24s
Hanzo CI/CD / cicd (push) Successful in 27m32s
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
# Conflicts: # apps/functions/billing_test.go |
||
|
|
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.
|
||
|
|
3d631516b1 |
exec: point at the namespace that has the Service
CI/CD / containment (push) Successful in 2m26s
Hanzo CI/CD / cicd (push) Failing after 32m42s
CI/CD / gate (push) Failing after 32m59s
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
I moved this to hanzo-sandboxes on the reasoning that everything running submitted code belongs in the one namespace whose policy denies it the cluster. That reasoning is right and the change was wrong: there is no code-exec Service in hanzo-sandboxes, so it renamed a 502 into a DNS failure. A peer caught it and said so plainly — target ns is EMPTY, do not ship. Moving the NAME before moving the SERVICE is the same mistake as a policy whose selector matches no pod, and this file would have been the fourth this week. The Service moves first; then the address follows it. Neither namespace makes this path work today — the Service in hanzo has had no endpoints for 33 days. The fix is not a better address, it is /v1/sandboxes, where a caller gets a pod rather than a proxy hop to a workload nobody deployed. |
||
|
|
6ae8b84563 |
sandbox: one word, one noun, one package
The box vocabulary is gone. It named the same thing three ways — a box in the Go, a machine in the types, a sandbox in the API — so a policy could select hanzo.ai/box-class while the code set hanzo.ai/sandbox-class and both looked right in isolation. That is not a style preference: it is how the containment shipped selecting nothing. apps/sandboxes -> apps/sandbox package sandbox, not sandboxes Machine -> Sandbox the type is the noun the API serves machines.go -> sandbox.go MACHINE_* -> SANDBOX_* /v1/sandboxes stays plural because a collection is plural. The PACKAGE is singular because it is one thing. DELETED, not deprecated: cmd/boxd (~2000 lines), the old apps/sandbox pool and proxy, apps/sandbox/wire. The daemon existed to serve a filesystem and a shell from inside the pod over HTTP; the Kubernetes exec subresource already does that, so boxd was a second implementation of a thing the cluster ships. Deleting it also deletes what it forced: a shared service key living in an environment the submitted code could read out of /proc, a bind protocol so a recycled pod IP could not serve another tenant's checkout, and an HTTP server that had to be built into every sandbox image. wire went with it, except for one string. It held the types boxd and the proxy both marshalled, which is a real reason for a package — but the last survivor is the /v1/exec path shared by apps/exec and apps/functions, and a package that exists to hold one constant is not a package. It moves to exec.Path, in the app that serves it. The reason it must have exactly one home is unchanged and worth keeping: those two consumers disagreed about it in production, one asking for /v1/exec and the other building upstream+"/exec", and because the executor did not exist neither was ever wrong out loud. Routes registered. Create, List, Get and Delete were exported functions no request could reach — a caller could exec in a sandbox it had no way to create. The comment said they were "shared with the compute surface", which was true while this served /v1/machines (visor's, where a second registration is a conflict) and false the moment it moved. Handlers present, routes absent, nothing saying so: the same shape as the policy that selected no pod and the installer that installed nothing. |
||
|
|
8374e8d214 |
Merge remote-tracking branch 'forge/main' into feat/sandbox-executor
# Conflicts: # apps/bots/typed_wire_test.go # apps/dns/typed_wire_test.go # apps/exec/typed_wire_test.go # apps/o11y/typed_wire_test.go # manifest/order_test.go |
||
|
|
9fd46ed039 |
the ledgers that named TRACE and OPTIONS, and a gate that read another commit
Four apps kept their own copy of the projection's method set and asserted it.
Dropping TRACE and OPTIONS from the document therefore surfaced as eight
failures that looked unrelated to it — bots and dns and o11y naming routes
"which this subsystem no longer serves", exec insisting on 56 operations where
40 are published.
Nothing about the WIRE changed. All() still binds every method and the executor
still answers them; exec's own subtest proves exactly that and is why it stayed.
What changed is what we OFFER, and the ledgers were describing the offer from a
second copy of the truth.
exec reads openapi.Methods() now, the same fix websearch took an hour ago and
for the same reason: one set, one place, both halves move together. The others
had literal entries, which are simply gone.
The counts move with it and say why in the prose beside them, so the next reader
gets the reason rather than a number that used to be right:
bots 10 -> 8 six product methods on the relay, plus two typed ops
exec 56 -> 40 8 paths x 5 published methods
SEPARATELY, and not caused by any of that: TestJWKSHasOneDerivation walks the
tree from ".", and git puts WORKTREES inside the working tree. It failed naming
.claude/worktrees/agent-…/apps/base/pool.go — a file from another commit of this
same repo, which this commit is not responsible for and cannot fix. It skips
.claude and .worktrees now. The gate could never fire in CI, which checks out
clean, so it only ever cost whoever had a worktree open — which, here, is
everyone running agents.
Verified against
|
||
|
|
471b7cad40 |
the route ledgers and the frozen order owe the same two changes
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
Same shape as the bots entry: a ledger describing a surface that moved, and a
freeze that a new app walked past. No behaviour changes.
|
||
|
|
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. |
||
|
|
8da312c255 |
exec: the shared pool lives where the policy is
apps/exec pointed at code-exec.hanzo.svc while apps/sandbox schedules boxes into hanzo-boxes, so the two halves of one executor sat in different namespaces under different containment — and the half in `hanzo` was the half sitting beside the datastores. They run the same binary on the same submitted code; only the lifetime differs, which is not a reason for two blast radii. Also corrects two comments that described the containment as absent. It was: the policy selected `app: code-exec` in namespace `hanzo` and matched no pod. universe now declares one policy over hanzo.ai/box-class in hanzo-boxes, so saying so is accurate rather than aspirational — which is the property those comments were written to protect in the first place. Co-authored-by: Hanzo Dev <dev@hanzo.ai> |
||
|
|
b4992c630e |
sandbox: the executor exists, is mounted, and ships in the image
Three things were true at once and each hid the next. cmd/boxd was written and untracked, so no release could contain it. Cloud's Dockerfile built /cloud and /smoke and nothing else, so committing boxd would still not have put it in an image. And apps/sandbox had a Mount with no plugin/sandbox beside it, so the package compiled, linked into nothing, and answered 404 in production while the code to answer sat in the tree. That is why `COPY --from=ghcr.io/hanzoai/cloud:<pin> /boxd` in hanzo/bot's Dockerfile.box was red by construction — grep -c boxd Dockerfile was 0 — and why every consumer already pointing at the executor (hanzo.app's ProjectFs, apps/exec's upstream, apps/functions' invoke) had nothing behind it. The 33-day-old code-exec Service with <none> endpoints is the same fact seen from the cluster. boxd builds HERE rather than in hanzo/bot because its types ARE apps/sandbox/wire's types: the scheduler and the daemon agree because they compile against one declaration, not because two repos were kept in sync by hand. It is copied into the FINAL layer, not just the build stage — a binary that exists only in the builder is not in the published image, and the box image reads the published one. boxd is also the half that runs untrusted code, so it refuses to start in the configuration where that code can steal the pool key: submitted work running as boxd's own uid can read CODE_EXEC_API_KEY out of /proc/<pid>/environ, and that key is shared across the pool, so the credential a box hands its own workload is the credential that opens every other tenant's box. Reproduced end to end. Two cheaper defenses do not work and pgroup_unix.go records why measurements, not reasoning: scrubbing the child's env misses that the child reads boxd's, and os.Unsetenv misses that the kernel serves /proc/environ from the exec-time stack block rather than the live environment. .gitignore takes boxd in both spellings. It is the one binary people build from inside its own directory, since it runs standalone on a laptop, and that file already carries what committing binaries cost this repo once. Co-authored-by: Hanzo Dev <dev@hanzo.ai> |
||
|
|
f6c9605bd7 |
zip v1.24.1: tests stop reaching through fiber, so they see what serving installs
Hanzo CI/CD / cicd (push) Successful in 18s
CI/CD / gate (push) Successful in 19s
CI/CD / containment (push) Successful in 1m38s
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
Upstream landed the v1.23 verb migration (Graft/Add/Mount folded into Use). This
is the half that was missing, and it is the half that made tests lie.
App.Test used to skip prepare, which installs the deferred projections — /mcp, the
OpenAPI document, the op-call plane, the plugin route. So those four addresses
answered 404 under test and 200 in production, and the papering-over was an
exported Prepare each caller had to remember. zip v1.24.1 makes Test prepare;
apps/ai's MCP door test passes because of that, not because of anything here.
414 call sites move from app.Fiber().Test(...) to app.Test(...) with
zip.TestConfig. That is the point of the escape hatch living on the concrete type:
reaching through it bypasses what App.Test does, so the tests most wanting to
exercise the real program were the ones that did not. Sites whose receiver is a
raw fiber app keep fiber's type — the two are not interchangeable and pretending
otherwise is how the first sweep broke things.
Also: the multi-line `Use(func(c *zip.Ctx) error {…})` literals in tests, which
the verb migration missed because they fail vet rather than build; and the last
`.Prepare()` calls, now that it is implicit.
iam v1.34.11 → v1.34.12.
Measured against upstream on the same host: 103 failing packages before, 97 after
— ZERO new, 6 fixed. The remainder is the macOS SQLCipher limit (no tmpfs for the
pure-Go codec), unrelated and unchanged.
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> |
||
|
|
0c56c7d92c |
agents: package docs name the product, and one package doc per package
Eleven packages in the agents cluster. Four docs said something false: benchmark and research cite an apps.go composition root deleted with the mega build, agentskills cites a go:generate path that moved, and eval/exec/bots/runtime/ connectorruntime/coding/automations cite clients/<app> packages that are now apps/<app>. Two packages carried TWO package docs — automations (automations.go + types.go) and runtime (runtime.go + ops.go). Go concatenates them in file order, so the product description an OpenAPI tag reads was 'types.go ports the ActivePieces shared contract' and 'ops.go mounts /v1/bot/*'. The second comment in each is now a file comment, separated from the package clause. exec's doc did not say it owns four top-level /v1 segments (/v1/exec, /v1/upload, /v1/download, /v1/files) and therefore publishes under four product tags; eval's led with the console fork it replaced instead of with the product; coding's led with 'the keystone that turns @hanzo from a chatbot into an engineer' and did not say it is a library with no route, no plugin and no manifest row. eval was missing GET /v1/evals/metrics, prompts GET /v1/prompts/catalog, and research its three /v1/research/artifacts ops. 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>
|
||
|
|
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> |
||
|
|
f873d1a180 |
apps: the 131 subsystems move from clients/ to apps/
They were never clients. A "client" is something that CALLS a service; these
are the subsystems the cloud binary mounts and serves. The name said the
opposite of what the code does, and it sat one directory above a composition
root already named `apps` — so the tree read as though `apps/` and `clients/`
were different kinds of thing when one is simply the wiring for the other.
`apps` is a package, so its subsystems nest under it directly:
apps/apps.go package apps — Wire() returns the 118 MountSpecs
apps/git/ package git
apps/projects/ package projects
`apps.Wire()` composing `apps/git` and `apps/projects` needs no second noun.
WHAT DID NOT MOVE. `clients/*.go` (package clients — aihttp, rpc, s3vfs) is
genuinely a client package: HTTP/RPC/VFS handles the subsystems dial OUT with.
It keeps the name, because for those six files the name was always right. Only
the 131 misfiled subdirectories moved, and the import rewrite is scoped to
`hanzoai/cloud/clients/<x>` so the surviving package is untouched.
This is a MOVE, not a rewrite, and deliberately so. mk/plugin.mk already
derives every path from its own location precisely so that "an extracted
apps/<app> + cmd/<app> + mk/ keeps these paths intact" — the build contract was
written for this migration before the directory was renamed to match it. 31
hanzoai/* modules are already extracted and wired as external imports, 16 of
them with thin in-repo adapters that import their own module (verified: zero
duplicates, no forked implementations). This rename puts the remaining 115 in
the directory the extraction contract already names.
Mechanical: git mv per subdir, then `hanzoai/cloud/clients/` ->
`hanzoai/cloud/apps/` across 656 Go files, plus 23 docs/Makefiles/manifests
rewritten only where the path names a real moved app. Builds clean; projects,
git and catalog tests pass.
(cmd/admission and cmd/affiliates fail to LINK here, before and after: they
need native/flags/target/release/libhanzo_flags.a, a Rust artifact never built
in this checkout. Pre-existing and unrelated.)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
|