Files
Hanzo Dev 01d3586493
Hanzo CI/CD / cicd (push) Successful in 22s
CI/CD / gate (push) Successful in 22s
CI/CD / containment (push) Successful in 1m13s
CI/CD / image (push) Successful in 17m59s
CI/CD / rollout (push) Failing after 12s
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Failing after 1s
openapi: a declaration belongs to the app that owns its address, not to the product segment
Complete judged an orphan by Product() — the first path segment after /v1/. A
product is not an app, and fourteen of them are answered by more than one,
because the manifest separates nested prefixes exactly as the router does.
/v1/s3 PROVISIONS an s3 resource (provisioning) while /v1/s3/buckets is the DATA
plane (storage), so storage was charged with provisioning's POST /v1/s3 — a
declaration provisioning both serves and describes — and could not project its
own document. surface-check died there, on an app with nothing wrong with it.

Attribute by manifest.OwnerOf instead: the longest-prefix rule the host itself
routes by, so the gate blames the app the request would actually reach. Asked
rather than re-derived, the two cannot drift.

It is passed IN rather than imported. manifest's own tests read this package to
pin the spec door's address, so an openapi that imported manifest back makes
manifest's test binary an import cycle — the compiler says so. nil is refused
rather than defaulted, because judging nothing is a gate that reports success on
every defect it exists to catch.

The host's own door is declined explicitly. owner answers a MANIFEST question,
and for /v1/openapi.json the manifest's answer is precisely the misroute
cmd/cloud's static route exists to correct; it is served by the host, mounted by
no app, and renders in the fleet document. Product() hid that by accident,
returning "" for any segment holding a dot.

The gate is unchanged where it matters. A declaration misfiled inside an app's
own prefix still fails — pinned here, and mutation-proven against apps/storage,
where an added GET /v1/s3/buckets/:bucket/files takes the run red while the
false positive stays gone.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-02 20:14:04 -07:00

176 lines
7.9 KiB
Go

package cloud
// `<binary> describe <dir>` — any cloud binary projects itself instead of serving.
//
// This is the ONE producer of a per-app artifact. An app's projections are not
// sliced out of the fleet's by prefix (that would make the fleet the source and
// the app a derivative, exactly backwards); they are generated from the app's OWN
// live router by the SAME openapi.FleetSpec the whole fleet is generated from,
// over an app with only that subsystem mounted.
// Compose upward, never carve downward — see openapi/weave.go for the other half.
//
// ONE projection, from ONE mount of ONE registry: openapi.json, what the app's
// addresses are.
//
// It used to write mcp.json beside it — the same registry projected as MCP tools
// — and argued that writing them together made them agree. They did agree, and
// they were BOTH WRONG BY THE SAME 353 OPS: coupling two derived files to each
// other makes neither true, because nothing in that coupling forces either back
// to the registry. The tool catalogue is not generated any more; the host asks
// the child for it (package fleet), so a tool list cannot be stale because there
// is no tool list. This document survives only because the fleet weave needs the
// app's SOURCE synopsis, which no running process can hand back.
//
// It lives on Serve because Serve is the single entry every app binary shares:
// plugin/<app>/main.go is generated as one cloud.Listen call, so putting the mode
// here gives every one of them the target at a cost of zero per-app code. A binary
// that mounts its own app (plugin/o11y) calls Describe directly.
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/hanzoai/cloud/manifest"
"github.com/hanzoai/cloud/openapi"
"github.com/zap-proto/zip"
)
// describeArg is the argv word that switches a binary from serving to describing.
const describeArg = "describe"
// SpecFile is the artifact a describe run writes into the app's own plugin/<app>
// directory. Named here because the host EMBEDS it (plugin/embed.go) and the
// weave READS it — one name, so a rename cannot leave a reader looking for a file
// no writer produces.
const SpecFile = "openapi.json"
// DescribeRequested reports whether argv asks this binary to describe itself, and
// the DIRECTORY it named: `<binary> describe <dir>`. Read before any flag parsing
// — the mode is a mode, not an option.
//
// A DIRECTORY and not stdout, and the argument is required. A subsystem's own
// dependencies write to stdout at mount (hanzoai/commerce prints a sqlite-vec
// warning and GORM debug lines), which a `> file` redirect splices into the front
// of the document and turns into 71KB of invalid JSON. A writer whose output an
// unrelated library can corrupt is not a writer.
func DescribeRequested() (string, bool) {
if len(os.Args) > 1 && os.Args[1] == describeArg {
if len(os.Args) > 2 {
return os.Args[2], true
}
return "", true
}
return "", false
}
// SpecConfig is the deployment the PUBLISHED artifacts describe, and the whole
// of it — zero values everywhere else, on purpose. The returned func removes the
// throwaway data dir.
//
// A published spec must be a function of the code alone. Config decides routes:
// clients/kms registers its secret routes only when a master key resolved, and
// several subsystems gate on brand. If this read the environment, the artifacts
// two developers generated from one commit would differ by whichever CLOUD_*
// variables their shells carried, and the golden would flap in CI for a reason
// no diff could explain. So it reads nothing.
//
// The data dir is a throwaway and is created HERE rather than taken as an
// argument, because mounting opens real stores: the default is /var/lib/cloud,
// and a caller that forgot to override it would either migrate a live store or
// (as cmd/o11y did) fail on it.
func SpecConfig() (*Config, func(), error) {
dir, err := os.MkdirTemp("", "openapi-spec-*")
if err != nil {
return nil, nil, err
}
return &Config{Brand: DefaultBrand, Domain: "api.hanzo.ai", DataDir: dir},
func() { os.RemoveAll(dir) }, nil
}
// Describe writes app's projection into dir: the OpenAPI document, from the one
// live router.
//
// JSON, because JSON is what it IS — the same bytes served at /v1/openapi.json
// and dropped into hanzoai/openapi. Encoding to YAML would put a yaml library in
// the graph of every app binary to write a file only the weave reads. Indented so
// a subset reviews as a diff.
//
// It is rendered whole before the file is touched, so a projection failure leaves
// the previous one intact rather than truncating it into an app that appears to
// serve nothing.
func Describe(dir string, app *zip.App) error {
if dir == "" {
return fmt.Errorf("usage: %s %s <dir>", filepath.Base(os.Args[0]), describeArg)
}
doc, err := openapi.FleetSpec(app)
if err != nil {
return fmt.Errorf("openapi: %w", err)
}
// The gap is refused HERE, at the one producer, and nowhere downstream.
//
// This is the file that mints the artifact eight SDKs, the MCP tool list, the
// CLI and docs.hanzo.ai are all projections of, so an operation that says
// nothing about itself becomes a call nobody can explain in every one of them
// at once — and each of those consumers is a place where the sentence cannot
// be written. A projection that copes (a placeholder, or the route printed
// where the description belongs) does not report the gap, it disguises it. So
// the document is simply not written: the failure names the app, the route and
// the remedy, in the repo that holds the handler.
//
// Refused at the artifact, not in [openapi.Spec], because the two have opposite
// duties. A deployment's own /v1/openapi.json must answer with what it serves
// even if a subsystem it mounts is behind on its prose; a COMMITTED artifact is
// the fleet's contract and has no such excuse.
//
// manifest.OwnerOf is handed in because "whose address is this?" is a ROUTING
// question, and the manifest is the routing table the host itself reads. Asking
// it here means the gate attributes a declaration exactly as the fleet delivers
// the request — the two cannot drift, and openapi stays a projection that owes
// nothing to the fleet's shape.
if err := openapi.Complete(doc, manifest.OwnerOf); err != nil {
return fmt.Errorf("%s: %w", filepath.Base(dir), err)
}
// A subset describes ONE app, so it says what that app is — the synopsis of
// the package its binary mounts, read from the source the app is built from
// (openapi.Synopsis). The fleet identity FleetSpec carries is the fallback and
// stays exactly that: an app whose package has no doc comment publishes the
// fleet's sentence, unchanged, rather than a sentence invented for it here.
//
// This is the ONE place the synopsis is computed. The weave reads it back off
// the subsets to describe the product tags, so the mapping from app to prose
// exists once and travels with the artifact.
if s := openapi.Synopsis(dir); s != "" {
doc.Info.Description = s
}
spec, err := json.MarshalIndent(doc, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(dir, SpecFile), append(spec, '\n'), 0o644)
}
// describe mounts specs into a throwaway app and writes its projections.
//
// Enablement is cfg's default, NOT the forced single-service list Serve applies:
// this is the fleet's document, and a STAGED subsystem (config.go's
// a deployment does not name) is linked but inert until it does. Describing
// one here would publish routes api.hanzo.ai does not serve, and would make the
// woven document disagree with the fully-mounted golden — which is the equality
// the composition proof rests on.
func describe(specs []Plugin, dir string) error {
cfg, done, err := SpecConfig()
if err != nil {
return err
}
defer done()
deps := BuildDeps(cfg)
app := zip.New(zip.Config{Logger: deps.Logger, DisableStartupMessage: true})
if err := MountAll(app, specs, cfg, deps); err != nil {
return err
}
return Describe(dir, app)
}